Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
require 'rails_helper'
RSpec.describe 'Api::V2::Accounts::LiveReports', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:team) { create(:team, account: account) }
let(:team_member) { create(:team_member, team: team, user: admin) }
describe 'GET /api/v2/accounts/{account.id}/live_reports/conversation_metrics' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated but not authorized' do
it 'returns forbidden' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated and authorized' do
before do
create(:conversation, :with_assignee, account: account, status: :open)
create(:conversation, account: account, status: :open)
create(:conversation, :with_assignee, account: account, status: :pending)
create(:conversation, :with_assignee, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns conversation metrics' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['open']).to eq(3)
expect(response_data['unattended']).to eq(2)
expect(response_data['unassigned']).to eq(1)
expect(response_data['pending']).to eq(1)
end
context 'with team_id parameter' do
before do
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open)
end
it 'returns metrics filtered by team' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
params: { team_id: team.id },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['open']).to eq(1)
expect(response_data['unattended']).to eq(1)
expect(response_data['unassigned']).to eq(1)
expect(response_data['pending']).to eq(0)
end
end
end
end
describe 'GET /api/v2/accounts/{account.id}/live_reports/grouped_conversation_metrics' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated but not authorized' do
it 'returns forbidden' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'with invalid group_by parameter' do
it 'returns unprocessable_entity error' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'invalid_param' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('invalid group_by')
end
end
context 'when grouped by team_id' do
let(:assignee1) { create(:user, account: account) }
before do
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open, team_id: team.id) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
create(:conversation, account: account, status: :open, assignee_id: assignee1.id)
create(:conversation, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns metrics grouped by team' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.size).to eq(2)
expect(response_data).to include(
{ 'team_id' => nil, 'open' => 2, 'unattended' => 1, 'unassigned' => 1 }
)
expect(response_data).to include(
{ 'team_id' => team.id, 'open' => 3, 'unattended' => 2, 'unassigned' => 3 }
)
end
end
context 'when filtering by assignee_id' do
let(:assignee1) { create(:user, account: account) }
before do
create(:conversation, assignee_id: agent.id, account: account, status: :open)
create(:conversation, account: account, status: :open)
create(:conversation, assignee_id: agent.id, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns metrics grouped by assignee' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'assignee_id' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.size).to eq 2
expect(response_data).to include(
{ 'assignee_id' => agent.id, 'open' => 2, 'unassigned' => 0, 'unattended' => 1 }
)
expect(response_data).to include(
{ 'assignee_id' => nil, 'open' => 1, 'unassigned' => 1, 'unattended' => 1 }
)
end
end
end
end

View File

@@ -0,0 +1,481 @@
require 'rails_helper'
RSpec.describe 'Reports API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:user) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
let(:default_timezone) { 'UTC' }
let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i }
let(:params) { { timezone_offset: Time.zone.utc_offset } }
let(:new_account) { create(:account) }
before do
create_list(:conversation, 10, account: account, inbox: inbox,
assignee: user, created_at: Time.current.in_time_zone(default_timezone).to_date)
end
describe 'GET /api/v2/accounts/:account_id/reports' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
metric: 'conversations_count',
type: :account,
since: start_of_today.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'return timeseries metrics' do
get "/api/v2/accounts/#{account.id}/reports",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
current_day_metric = json_response.select { |x| x['timestamp'] == start_of_today }
expect(current_day_metric.length).to eq(1)
expect(current_day_metric[0]['value']).to eq(10)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/conversations' do
context 'when it is an authenticated user' do
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 = response.parsed_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)
create_list(:conversation, 2, account: new_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 = response.parsed_body
expect(json_response.blank?).to be false
user_metrics = json_response.find { |item| item['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
end
context 'when an agent1 associated to conversation having first reply from agent2' do
let(:listener) { ReportingEventListener.instance }
let(:account) { create(:account) }
let(:agent2) { create(:user, account: account, role: :agent) }
it 'returns unattended conversation count zero for agent1' do
create(:inbox_member, user: agent, inbox: inbox)
create(:inbox_member, user: agent2, inbox: inbox)
conversation = create(:conversation, account: account,
inbox: inbox, assignee: agent2)
create(:message, message_type: 'incoming', content: 'Hi',
account: account, inbox: inbox,
conversation: conversation)
first_reply_message = create(:message, message_type: 'outgoing', content: 'Hi',
account: account, inbox: inbox, sender: agent2,
conversation: conversation)
event = Events::Base.new('first.reply.created', Time.zone.now, message: first_reply_message)
listener.first_reply_created(event)
conversation.assignee_id = agent.id
conversation.save!
get "/api/v2/accounts/#{account.id}/reports/conversations",
params: {
type: :agent
},
headers: admin.create_new_auth_token,
as: :json
json_response = response.parsed_body
user_metrics = json_response.find { |item| item['name'] == agent[:name] }
expect(user_metrics.present?).to be true
expect(user_metrics['metric']['open']).to eq(1)
expect(user_metrics['metric']['unattended']).to eq(0)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/summary' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/summary"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
type: :account,
since: start_of_today.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary metrics' do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['conversations_count']).to eq(10)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/bot_summary' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/bot_summary"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
type: :account,
since: start_of_today.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports/bot_summary",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns bot summary metrics' do
get "/api/v2/accounts/#{account.id}/reports/bot_summary",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['bot_resolutions_count']).to eq(0)
expect(json_response['bot_handoffs_count']).to eq(0)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/agents' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/agents.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 30.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
context 'when an agent has access to multiple accounts' do
let(:account1) { create(:account) }
let(:account2) { create(:account) }
let(:params) do
super().merge(
type: :agent,
since: 30.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns agent metrics from the current account' do
admin1 = create(:user, account: account1, role: :administrator)
inbox1 = create(:inbox, account: account1)
inbox2 = create(:inbox, account: account2)
create(:account_user, user: admin1, account: account2)
create(:conversation, account: account1, inbox: inbox1,
assignee: admin1, created_at: Time.zone.today - 2.days)
create(:conversation, account: account2, inbox: inbox2,
assignee: admin1, created_at: Time.zone.today - 2.days)
get "/api/v2/accounts/#{account1.id}/reports/summary",
params: params.merge({ id: admin1.id }),
headers: admin1.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['conversations_count']).to eq(1)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/inboxes' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 30.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for inboxes' do
get "/api/v2/accounts/#{account.id}/reports/inboxes",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v2/accounts/#{account.id}/reports/inboxes",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/labels' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 30.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for labels' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/teams' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/teams.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 30.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized for teams' do
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/conversation_traffic' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 7.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns values' do
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/bot_metrics' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/bot_metrics"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
super().merge(
since: 7.days.ago.to_i.to_s,
until: end_of_today.to_s
)
end
it 'returns unauthorized if the user is an agent' do
get "/api/v2/accounts/#{account.id}/reports/bot_metrics",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns values' do
expect(V2::Reports::BotMetricsBuilder).to receive(:new).and_call_original
get "/api/v2/accounts/#{account.id}/reports/bot_metrics",
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
expect(response.parsed_body.keys).to match_array(%w[conversation_count message_count resolution_rate handoff_rate])
end
end
end
end

View File

@@ -0,0 +1,199 @@
require 'rails_helper'
RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:inbox) { create(:inbox, account: account) }
describe 'GET /api/v2/accounts/{account.id}/reports' do
context 'when authenticated and authorized' do
before do
# Create conversations across 24 hours at different times
base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days
# Create conversations every 4 hours across 24 hours
6.times do |i|
time = base_time + (i * 4).hours
travel_to time do
conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
end
it 'timezone_offset affects data grouping and timestamps correctly' do
travel_to Time.utc(2024, 1, 15, 12, 0) do
Time.use_zone('UTC') do
base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days
base_params = {
metric: 'conversations_count',
type: 'account',
since: (base_time - 1.day).to_i.to_s,
until: (base_time + 2.days).to_i.to_s,
group_by: 'day'
}
responses = [0, -8, 9].map do |offset|
get "/api/v2/accounts/#{account.id}/reports",
params: base_params.merge(timezone_offset: offset),
headers: admin.create_new_auth_token, as: :json
response.parsed_body
end
data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } }
totals = responses.map { |r| r.sum { |e| e['value'] } }
timestamps = responses.map { |r| r.map { |e| e['timestamp'] } }
# Data conservation and redistribution
expect(totals.uniq).to eq([6])
expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5])
expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3])
expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2])
# Timestamp differences
expect(timestamps.uniq.size).to eq(3)
timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) }
end
end
end
describe 'timezone_offset does not affect summary report totals' do
let(:base_time) { Time.utc(2024, 1, 15, 12, 0) }
let(:summary_params) do
{
type: 'account',
since: (base_time - 1.day).to_i.to_s,
until: (base_time + 1.day).to_i.to_s
}
end
let(:jst_params) do
# For JST: User wants "Jan 15 JST" which translates to:
# Jan 14 15:00 UTC to Jan 15 15:00 UTC (event NOT included)
{
type: 'account',
since: (Time.utc(2024, 1, 15, 0, 0) - 9.hours).to_i.to_s, # Jan 14 15:00 UTC
until: (Time.utc(2024, 1, 16, 0, 0) - 9.hours).to_i.to_s # Jan 15 15:00 UTC
}
end
let(:utc_params) do
# For UTC: Jan 15 00:00 UTC to Jan 16 00:00 UTC (event included)
{
type: 'account',
since: Time.utc(2024, 1, 15, 0, 0).to_i.to_s,
until: Time.utc(2024, 1, 16, 0, 0).to_i.to_s
}
end
it 'returns identical conversation counts across timezones' do
Time.use_zone('UTC') do
summaries = [-8, 0, 9].map do |offset|
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: offset),
headers: admin.create_new_auth_token, as: :json
response.parsed_body
end
conversation_counts = summaries.map { |s| s['conversations_count'] }
expect(conversation_counts.uniq).to eq([6])
end
end
it 'returns identical message counts across timezones' do
Time.use_zone('UTC') do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: 0),
headers: admin.create_new_auth_token, as: :json
utc_summary = response.parsed_body
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: -8),
headers: admin.create_new_auth_token, as: :json
pst_summary = response.parsed_body
expect(utc_summary['incoming_messages_count']).to eq(pst_summary['incoming_messages_count'])
expect(utc_summary['outgoing_messages_count']).to eq(pst_summary['outgoing_messages_count'])
end
end
it 'returns consistent resolution counts across timezones' do
Time.use_zone('UTC') do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: 0),
headers: admin.create_new_auth_token, as: :json
utc_summary = response.parsed_body
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: 9),
headers: admin.create_new_auth_token, as: :json
jst_summary = response.parsed_body
expect(utc_summary['resolutions_count']).to eq(jst_summary['resolutions_count'])
end
end
it 'returns consistent previous period data across timezones' do
Time.use_zone('UTC') do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: 0),
headers: admin.create_new_auth_token, as: :json
utc_summary = response.parsed_body
get "/api/v2/accounts/#{account.id}/reports/summary",
params: summary_params.merge(timezone_offset: -8),
headers: admin.create_new_auth_token, as: :json
pst_summary = response.parsed_body
expect(utc_summary['previous']['conversations_count']).to eq(pst_summary['previous']['conversations_count']) if utc_summary['previous']
end
end
it 'summary reports work when frontend sends correct timezone boundaries' do
Time.use_zone('UTC') do
# Create a resolution event right at timezone boundary
boundary_time = Time.utc(2024, 1, 15, 23, 30) # 11:30 PM UTC on Jan 15
gravatar_url = 'https://www.gravatar.com'
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
travel_to boundary_time do
perform_enqueued_jobs do
conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
conversation.resolved!
end
end
get "/api/v2/accounts/#{account.id}/reports/summary",
params: jst_params.merge(timezone_offset: 9),
headers: admin.create_new_auth_token, as: :json
jst_summary = response.parsed_body
get "/api/v2/accounts/#{account.id}/reports/summary",
params: utc_params.merge(timezone_offset: 0),
headers: admin.create_new_auth_token, as: :json
utc_summary = response.parsed_body
expect(jst_summary['resolutions_count']).to eq(0)
expect(utc_summary['resolutions_count']).to eq(1)
end
end
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated but not authorized' do
it 'returns forbidden' do
get "/api/v2/accounts/#{account.id}/reports",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,227 @@
require 'rails_helper'
RSpec.describe 'Summary Reports API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:default_timezone) { ActiveSupport::TimeZone[0]&.name }
let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i }
describe 'GET /api/v2/accounts/:account_id/summary_reports/agent' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/summary_reports/agent"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
since: start_of_today.to_s,
until: end_of_today.to_s,
business_hours: true
}
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/summary_reports/agent",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'calls V2::Reports::AgentSummaryBuilder with the right params if the user is an admin' do
agent_summary_builder = double
allow(V2::Reports::AgentSummaryBuilder).to receive(:new).and_return(agent_summary_builder)
allow(agent_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
get "/api/v2/accounts/#{account.id}/summary_reports/agent",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(V2::Reports::AgentSummaryBuilder).to have_received(:new).with(account: account, params: params)
expect(agent_summary_builder).to have_received(:build)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(1)
expect(json_response.first['conversations_count']).to eq(110)
expect(json_response.first['avg_reply_time']).to be_nil
end
end
end
describe 'GET /api/v2/accounts/:account_id/summary_reports/inbox' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/summary_reports/inbox"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
since: start_of_today.to_s,
until: end_of_today.to_s,
business_hours: true
}
end
it 'returns unauthorized for inbox' do
get "/api/v2/accounts/#{account.id}/summary_reports/inbox",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'calls V2::Reports::InboxSummaryBuilder with the right params if the user is an admin' do
inbox_summary_builder = double
allow(V2::Reports::InboxSummaryBuilder).to receive(:new).and_return(inbox_summary_builder)
allow(inbox_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
get "/api/v2/accounts/#{account.id}/summary_reports/inbox",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(V2::Reports::InboxSummaryBuilder).to have_received(:new).with(account: account, params: params)
expect(inbox_summary_builder).to have_received(:build)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(1)
expect(json_response.first['conversations_count']).to eq(110)
expect(json_response.first['avg_reply_time']).to be_nil
end
end
end
describe 'GET /api/v2/accounts/:account_id/summary_reports/team' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/summary_reports/team"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
since: start_of_today.to_s,
until: end_of_today.to_s,
business_hours: true
}
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/summary_reports/team",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'calls V2::Reports::TeamSummaryBuilder with the right params if the user is an admin' do
team_summary_builder = double
allow(V2::Reports::TeamSummaryBuilder).to receive(:new).and_return(team_summary_builder)
allow(team_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
get "/api/v2/accounts/#{account.id}/summary_reports/team",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(V2::Reports::TeamSummaryBuilder).to have_received(:new).with(account: account, params: params)
expect(team_summary_builder).to have_received(:build)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(1)
expect(json_response.first['conversations_count']).to eq(110)
expect(json_response.first['avg_reply_time']).to be_nil
end
end
end
describe 'GET /api/v2/accounts/:account_id/summary_reports/channel' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/summary_reports/channel"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
since: start_of_today.to_s,
until: end_of_today.to_s
}
end
it 'returns unauthorized for agents' do
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'calls V2::Reports::ChannelSummaryBuilder with the right params if the user is an admin' do
channel_summary_builder = double
allow(V2::Reports::ChannelSummaryBuilder).to receive(:new).and_return(channel_summary_builder)
allow(channel_summary_builder).to receive(:build)
.and_return({
'Channel::WebWidget' => { open: 5, resolved: 10, pending: 2, snoozed: 1, total: 18 }
})
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(V2::Reports::ChannelSummaryBuilder).to have_received(:new).with(
account: account,
params: hash_including(since: start_of_today.to_s, until: end_of_today.to_s)
)
expect(channel_summary_builder).to have_received(:build)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['Channel::WebWidget']['open']).to eq(5)
expect(json_response['Channel::WebWidget']['total']).to eq(18)
end
it 'returns unprocessable_entity when date range exceeds 6 months' do
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
params: { since: 1.year.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(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.reports.date_range_too_long'))
end
end
end
end

View File

@@ -0,0 +1,118 @@
require 'rails_helper'
RSpec.describe 'Accounts API', type: :request do
describe 'POST /api/v2/accounts' do
let(:email) { Faker::Internet.email }
context 'when posting to accounts with correct parameters' do
let(:account_builder) { double }
let(:account) { create(:account) }
let(:user) { create(:user, email: email, account: account) }
before do
allow(AccountBuilder).to receive(:new).and_return(account_builder)
end
it 'calls account builder' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
post api_v2_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
expect(response.body).to include('en')
end
end
it 'updates the onboarding step in custom attributes' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
post api_v2_accounts_url,
params: params,
as: :json
expect(account.reload.custom_attributes['onboarding_step']).to eq('profile_update')
end
end
it 'calls ChatwootCaptcha' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
captcha = double
allow(account_builder).to receive(:perform).and_return([user, account])
allow(ChatwootCaptcha).to receive(:new).and_return(captcha)
allow(captcha).to receive(:valid?).and_return(true)
params = { email: email, user: nil, password: 'Password1!', locale: nil, h_captcha_client_response: '123' }
post api_v2_accounts_url,
params: params,
as: :json
expect(ChatwootCaptcha).to have_received(:new).with('123')
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
expect(response.body).to include('en')
end
end
it 'renders error response on invalid params' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return(nil)
params = { email: nil, user: nil, locale: nil }
post api_v2_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json)
end
end
end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do
it 'responds 404 on requests' do
params = { email: email }
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
post api_v2_accounts_url,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do
let(:account_builder) { double }
let(:account) { create(:account) }
let(:user) { create(:user, email: email, account: account) }
it 'does not respond 404 on requests' do
allow(AccountBuilder).to receive(:new).and_return(account_builder)
allow(account_builder).to receive(:perform).and_return([user, account])
params = { email: email, user: nil, password: 'Password1!', locale: nil }
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'api_only' do
post api_v2_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(response).to have_http_status(:success)
end
end
end
end
end