Skip to content

Commit

Permalink
feat: conversation export job direct to email (#55)
Browse files Browse the repository at this point in the history
* feat: job to send conversation report to admins

* feat: renabled the download conversation export button on frontend

* feat: psql statement timeout to 60s for DailyConversationReportJob

* feat: added conversation job to hulalahome

* feat: shopurl instead of name in intercom

* feat: fetch job data from URL in DailyConversationReportJob

* fix: set_statement_timeout should be called when generating a report
  • Loading branch information
JaideepGuntupalli authored Jun 18, 2024
1 parent 95981f7 commit b2834fb
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 116 deletions.
86 changes: 9 additions & 77 deletions app/helpers/api/v2/accounts/reports_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# rubocop:disable Metrics/ModuleLength
module Api::V2::Accounts::ReportsHelper
def generate_agents_report
Current.account.users.map do |agent|
Expand All @@ -8,22 +7,15 @@ def generate_agents_report
end

def generate_conversations_report
conversation_reports = generate_conversation_report(Current.account.id, range)
conversation_reports.map do |conversation_report|
[
conversation_report['conversation_display_id'],
conversation_report['inbox_name'],
conversation_report['customer_name'],
conversation_report['customer_phone_number'],
conversation_report['customer_email'],
conversation_report['customer_instagram_handle'],
conversation_report['agent_name'],
conversation_report['conversation_status'],
conversation_report['first_response_time_minutes'],
conversation_report['resolution_time_minutes'],
conversation_report['labels']
]
end
current_range = range[:current]
range_start = DateTime.strptime(current_range[:since].to_s, '%s')
range_end = DateTime.strptime(current_range[:until].to_s, '%s')
sanitized_range = { since: range_start, until: range_end }

# background job to generate daily conversation report and mail it to the user
DailyConversationReportJob.new.generate_custom_report(Current.account.id, sanitized_range)

[]
end

def generate_inboxes_report
Expand Down Expand Up @@ -77,64 +69,4 @@ def generate_readable_report_metrics(report_metric)
report_metric[:resolutions_count]
]
end

# rubocop:disable Metrics/MethodLength
def generate_conversation_report(account_id, range)
current_range = range[:current]
range_start = DateTime.strptime(current_range[:since].to_s, '%s')
range_end = DateTime.strptime(current_range[:until].to_s, '%s')
key = params[:business_hours].present? && (params[:business_hours] == 'true') ? 'value_in_business_hours' : 'value'

Rails.logger.info "key_for_query: #{key}"
Rails.logger.info "params[:business_hours]: #{params[:business_hours]}"

# Using ActiveRecord::Base directly for sanitization
sql = <<-SQL.squish
SELECT DISTINCT
conversations.id AS conversation_id,
conversations.display_id AS conversation_display_id,
conversations.created_at AS created_at,
contacts.created_at AS customer_created_at,
inboxes.name AS inbox_name,
contacts.name AS customer_name,
REPLACE(contacts.phone_number, '+', '') AS customer_phone_number,
contacts.email AS customer_email,
contacts.additional_attributes ->> 'social_instagram_user_name' AS customer_instagram_handle,
users.name AS agent_name,
CASE
WHEN conversations.status = 0 THEN 'open'
WHEN conversations.status = 1 THEN 'resolved'
WHEN conversations.status = 2 THEN 'pending'
WHEN conversations.status = 3 THEN 'snoozed'
END AS conversation_status,
reporting_events_first_response.#{key} / 60.0 AS first_response_time_minutes,
latest_conversation_resolved.#{key} / 60.0 AS resolution_time_minutes,
conversations.cached_label_list AS labels
FROM
conversations
JOIN inboxes ON conversations.inbox_id = inboxes.id
JOIN contacts ON conversations.contact_id = contacts.id
JOIN account_users ON conversations.assignee_id = account_users.user_id
JOIN users ON account_users.user_id = users.id
LEFT JOIN reporting_events AS reporting_events_first_response
ON conversations.id = reporting_events_first_response.conversation_id
AND reporting_events_first_response.name = 'first_response'
LEFT JOIN LATERAL (
SELECT #{key}
FROM reporting_events AS re
WHERE re.conversation_id = conversations.id
AND re.name = 'conversation_resolved'
ORDER BY re.created_at DESC
LIMIT 1
) AS latest_conversation_resolved ON true
WHERE
conversations.account_id = $1
AND conversations.updated_at BETWEEN $2 AND $3
SQL

ActiveRecord::Base.connection.exec_query(sql, 'SQL', [account_id, range_start, range_end]).to_a
end

# rubocop:enable Metrics/MethodLength
end
# rubocop:enable Metrics/ModuleLength
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<div class="flex-1 overflow-auto p-4">
<!-- <woot-button
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="arrow-download"
@click="downloadConversationReports"
>
{{ 'Download conversation reports' }}
</woot-button> -->
</woot-button>
<report-filter-selector
:show-agents-filter="false"
:show-group-by-filter="true"
Expand Down Expand Up @@ -102,6 +102,10 @@ export default {
};
},
downloadConversationReports() {
this.showAlert(
'The report will soon be available in all administrator email inboxes.',
'info'
);
const { from, to, businessHours } = this;
const fileName = `conversations-report-${format(
fromUnixTime(to),
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/dashboard/store/modules/reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ export const actions = {
},
downloadConversationReports(_, reportObj) {
return Report.getConversationReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
.then(() => {
// downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'conversation',
businessHours: reportObj?.businessHours,
Expand Down
75 changes: 45 additions & 30 deletions app/jobs/daily_conversation_report_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,70 @@
class DailyConversationReportJob < ApplicationJob
queue_as :scheduled_jobs

JOB_DATA_URL = 'https://bitespeed-app.s3.amazonaws.com/InternalAccess/cw-auto-conversation-report.json'.freeze

def perform
job_data = [{
account_id: 138,
frequency: 'daily'
}, {
account_id: 138,
frequency: 'weekly' # should trigger only on Mondays
},
{
account_id: 504,
frequency: 'daily'
}]
set_statement_timeout

# fetching the job data from the URL
response = HTTParty.get(JOB_DATA_URL)
job_data = JSON.parse(response.body, symbolize_names: true)

job_data.each do |job|
current_date = Date.current
current_day = current_date.wday

next if job[:frequency] == 'weekly' && current_day != 0
# should trigger only on Mondays
next if job[:frequency] == 'weekly' && current_day != 1

current_date = Date.current

process_account(job[:account_id], current_date, job[:frequency])
range = if job[:frequency] == 'weekly'
{ since: 1.week.ago, until: Time.current }
else
{ since: 1.day.ago, until: Time.current }
end

process_account(job[:account_id], current_date, range, job[:frequency])
end
end

def generate_custom_report(account_id, range)
set_statement_timeout

current_date = Date.current

process_account(account_id, current_date, range, 'custom')
end

private

def process_account(account_id, current_date, frequency)
report = generate_report(account_id, frequency)
def set_statement_timeout
ActiveRecord::Base.connection.execute("SET statement_timeout = '60s'")
end

def process_account(account_id, _current_date, range, frequency = 'daily')
report = generate_report(account_id, range)

if report.present?
Rails.logger.info "Data found for account_id: #{account_id}"

csv_content = generate_csv(report)
upload_csv(account_id, current_date, csv_content, frequency)
start_date = range[:since].strftime('%Y-%m-%d')
end_date = range[:until].strftime('%Y-%m-%d')

csv_content = generate_csv(report, start_date, end_date)
upload_csv(account_id, start_date, end_date, csv_content, frequency)
else
Rails.logger.info "No data found for account_id: #{account_id}"
end
end

# rubocop:disable Metrics/MethodLength
def generate_report(account_id, frequency = 'daily')
range = if frequency == 'weekly'
{ since: 1.week.ago, until: Time.current }
else
{ since: 1.day.ago, until: Time.current }
end

def generate_report(account_id, range)
# Using ActiveRecord::Base directly for sanitization
sql = ActiveRecord::Base.send(:sanitize_sql_array, [<<-SQL.squish, { account_id: account_id, since: range[:since], until: range[:until] }])
SELECT
conversations.id AS conversation_id,
distinct conversations.id AS conversation_id,
conversations.display_id AS conversation_display_id,
conversations.created_at AS conversation_created_at,
contacts.created_at AS customer_created_at,
Expand Down Expand Up @@ -98,8 +110,9 @@ def generate_report(account_id, frequency = 'daily')
end
# rubocop:enable Metrics/MethodLength

def generate_csv(results)
def generate_csv(results, start_date, end_date)
CSV.generate(headers: true) do |csv|
csv << ["Reporting period #{start_date} to #{end_date}"]
csv << [
'Conversation ID', 'Conversation Created At', 'Contact Created At', 'Inbox Name',
'Customer Phone Number', 'Customer Name', 'Agent Name', 'Conversation Status',
Expand All @@ -115,9 +128,9 @@ def generate_csv(results)
end
end

def upload_csv(account_id, current_date, csv_content, frequency)
def upload_csv(account_id, start_date, end_date, csv_content, frequency)
# Determine the file name based on the frequency
file_name = "#{frequency}_conversation_report_#{account_id}_#{current_date}.csv"
file_name = "#{frequency}_conversation_report_#{account_id}_#{end_date}.csv"

# For testing locally, uncomment below
# puts csv_content
Expand All @@ -137,9 +150,11 @@ def upload_csv(account_id, current_date, csv_content, frequency)
mailer = AdministratorNotifications::ChannelNotificationsMailer.with(account: Account.find(account_id))

if frequency == 'weekly'
mailer.weekly_conversation_report(csv_url, current_date - 7.days, current_date).deliver_now
mailer.weekly_conversation_report(csv_url, start_date, end_date).deliver_now
elsif frequency == 'daily'
mailer.daily_conversation_report(csv_url, end_date).deliver_now
else
mailer.daily_conversation_report(csv_url, current_date).deliver_now
mailer.custom_conversation_report(csv_url, start_date, end_date).deliver_now
end
end
end
10 changes: 10 additions & 0 deletions app/jobs/test_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# class TestJob < ApplicationJob
# queue_as :default

# def perform(*args)
# # Do something later
# puts "TestJob is running"

# DailyConversationReportJob.new.generate_custom_report(785, { since: 12.month.ago, until: Time.current })
# end
# end
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ def weekly_conversation_report(csv_url, since_date, until_date)
send_mail_with_liquid(to: admin_emails + ['jaideep+chatwootreports@bitespeed.co'], subject: subject) and return
end

def custom_conversation_report(csv_url, since_date, until_date)
return unless smtp_config_set_or_development?

subject = "Conversation Report from #{since_date} to #{until_date}"
@action_url = csv_url
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end

def contact_import_failed
return unless smtp_config_set_or_development?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<p>Hello,</p>

<p>Please find attached the conversation report you requested.</p>

<p>
Click <a href="{{action_url}}">here</a> to download.
</p>

<p>Regards,<br>BiteSpeed</p>
20 changes: 15 additions & 5 deletions config/initializers/intercom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,25 @@
# A Proc that given a user returns true if the user should be excluded
# from imports and Javascript inclusion, false otherwise.
#
config.user.exclude_if = proc { |user| user.type == 'SuperAdmin'}
config.user.exclude_if = proc { |user| user.type == 'SuperAdmin' }

# == User Custom Data
# A hash of additional data you wish to send about your users.
# You can provide either a method name which will be sent to the current
# user object, or a Proc which will be passed the current user.
#
config.user.custom_data = {
:user_type => proc { |current_user| current_user.type },
:user_auth_provider => proc { |current_user| current_user.provider }
}
# config.user.custom_data = {
# :user_type => proc { |current_user| current_user.type },
# :user_auth_provider => proc { |current_user| current_user.provider }
# }

# == Current company method/variable
# The method/variable that contains the current company for the current user,
# in your controllers. 'Companies' are generic groupings of users, so this
# could be a company, app or group.
#
shop_url = nil

config.company.current = proc {
## This drops us in `DashboardController#index` and so there isn't much in the way of getting current account
account_id = params[:params].split('/')[1].to_i ## when URLs are of form `accounts/<account_id>/blah`
Expand All @@ -79,6 +81,8 @@

account = JSON.parse(res.body)['accountDetails']

shop_url = account['shopUrl']

Struct.new(:id, :name, :company_website).new(account['accountId'], account['accountName'], account['shopUrl'])
}
#
Expand All @@ -101,6 +105,12 @@
:shop_name => proc { |current_company| current_company.name }
}

# update user's name to company_website
config.user.custom_data = {
:name => proc { shop_url },
:user_type => proc { |current_user| current_user.type },
:user_auth_provider => proc { |current_user| current_user.provider }
}
# == Company Plan name
# This is the name of the plan a company is currently paying (or not paying) for.
# e.g. Messaging, Free, Pro, etc.
Expand Down

0 comments on commit b2834fb

Please sign in to comment.