Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,158 @@
## Class to generate sample data for a chatwoot test @Account.
############################################################
### Usage #####
#
# # Seed an account with all data types in this class
# Seeders::AccountSeeder.new(account: Account.find(1)).perform!
#
#
############################################################
class Seeders::AccountSeeder
def initialize(account:)
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
@account_data = ActiveSupport::HashWithIndifferentAccess.new(YAML.safe_load(Rails.root.join('lib/seeders/seed_data.yml').read))
@account = account
end
def perform!
set_up_account
seed_teams
seed_custom_roles
set_up_users
seed_labels
seed_canned_responses
seed_inboxes
seed_contacts
end
def set_up_account
@account.teams.destroy_all
@account.conversations.destroy_all
@account.labels.destroy_all
@account.inboxes.destroy_all
@account.contacts.destroy_all
@account.custom_roles.destroy_all if @account.respond_to?(:custom_roles)
end
def seed_teams
@account_data['teams'].each do |team_name|
@account.teams.create!(name: team_name)
end
end
def seed_custom_roles
return unless @account_data['custom_roles'].present? && @account.respond_to?(:custom_roles)
@account_data['custom_roles'].each do |role_data|
@account.custom_roles.create!(
name: role_data['name'],
description: role_data['description'],
permissions: role_data['permissions']
)
end
end
def seed_labels
@account_data['labels'].each do |label|
@account.labels.create!(label)
end
end
def set_up_users
@account_data['users'].each do |user|
user_record = create_user_record(user)
create_account_user(user_record, user)
add_user_to_teams(user: user_record, teams: user['team']) if user['team'].present?
end
end
private
def create_user_record(user)
user_record = User.create_with(name: user['name'], password: 'Password1!.').find_or_create_by!(email: user['email'].to_s)
user_record.skip_confirmation!
user_record.save!
Avatar::AvatarFromUrlJob.perform_later(user_record, "https://xsgames.co/randomusers/avatar.php?g=#{user['gender']}")
user_record
end
def create_account_user(user_record, user)
account_user_attrs = build_account_user_attrs(user)
AccountUser.create_with(account_user_attrs).find_or_create_by!(account_id: @account.id, user_id: user_record.id)
end
def build_account_user_attrs(user)
attrs = { role: (user['role'] || 'agent') }
custom_role = find_custom_role(user['custom_role']) if user['custom_role'].present?
attrs[:custom_role] = custom_role if custom_role
attrs
end
def add_user_to_teams(user:, teams:)
teams.each do |team|
team_record = @account.teams.where('name LIKE ?', "%#{team.downcase}%").first if team.present?
TeamMember.find_or_create_by!(team_id: team_record.id, user_id: user.id) unless team_record.nil?
end
end
def find_custom_role(role_name)
return nil unless @account.respond_to?(:custom_roles)
@account.custom_roles.find_by(name: role_name)
end
def seed_canned_responses(count: 50)
count.times do
@account.canned_responses.create(content: Faker::Quote.fortune_cookie, short_code: Faker::Alphanumeric.alpha(number: 10))
end
end
def seed_contacts
@account_data['contacts'].each do |contact_data|
contact = @account.contacts.find_or_initialize_by(email: contact_data['email'])
if contact.new_record?
contact.update!(contact_data.slice('name', 'email'))
Avatar::AvatarFromUrlJob.perform_later(contact, "https://xsgames.co/randomusers/avatar.php?g=#{contact_data['gender']}")
end
contact_data['conversations'].each do |conversation_data|
inbox = @account.inboxes.find_by(channel_type: conversation_data['channel'])
contact_inbox = inbox.contact_inboxes.create_or_find_by!(contact: contact, source_id: (conversation_data['source_id'] || SecureRandom.hex))
create_conversation(contact_inbox: contact_inbox, conversation_data: conversation_data)
end
end
end
def create_conversation(contact_inbox:, conversation_data:)
assignee = User.from_email(conversation_data['assignee']) if conversation_data['assignee'].present?
conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact,
inbox: contact_inbox.inbox, assignee: assignee)
create_messages(conversation: conversation, messages: conversation_data['messages'])
conversation.update_labels(conversation_data[:labels]) if conversation_data[:labels].present?
conversation.update!(priority: conversation_data[:priority]) if conversation_data[:priority].present?
end
def create_messages(conversation:, messages:)
messages.each do |message_data|
sender = find_message_sender(conversation, message_data)
conversation.messages.create!(
message_data.slice('content', 'message_type').merge(
account: conversation.inbox.account, sender: sender, inbox: conversation.inbox
)
)
end
end
def find_message_sender(conversation, message_data)
if message_data['message_type'] == 'incoming'
conversation.contact
elsif message_data['sender'].present?
User.from_email(message_data['sender'])
end
end
def seed_inboxes
Seeders::InboxSeeder.new(account: @account, company_data: @account_data[:company]).perform!
end
end

View File

@@ -0,0 +1,105 @@
## Class to generate sample inboxes for a chatwoot test @Account.
############################################################
### Usage #####
#
# # Seed an account with all data types in this class
# Seeders::InboxSeeder.new(account: @Account.find(1), company_data: {name: 'PaperLayer', doamin: 'paperlayer.test'}).perform!
#
#
############################################################
class Seeders::InboxSeeder
def initialize(account:, company_data:)
raise 'Inbox Seeding is not allowed in production.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
@account = account
@company_data = company_data
end
def perform!
seed_website_inbox
seed_facebook_inbox
seed_twitter_inbox
seed_whatsapp_inbox
seed_sms_inbox
seed_email_inbox
seed_api_inbox
seed_telegram_inbox
seed_line_inbox
end
def seed_website_inbox
channel = Channel::WebWidget.create!(account: @account, website_url: "https://#{@company_data['domain']}")
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Website")
end
def seed_facebook_inbox
channel = Channel::FacebookPage.create!(account: @account, user_access_token: SecureRandom.hex, page_access_token: SecureRandom.hex,
page_id: SecureRandom.hex)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Facebook")
end
def seed_twitter_inbox
channel = Channel::TwitterProfile.create!(account: @account, twitter_access_token: SecureRandom.hex,
twitter_access_token_secret: SecureRandom.hex, profile_id: '123')
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Twitter")
end
def seed_whatsapp_inbox
# rubocop:disable Rails/SkipsModelValidations
Channel::Whatsapp.insert(
{
account_id: @account.id,
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
created_at: Time.now.utc,
updated_at: Time.now.utc
},
returning: %w[id]
)
# rubocop:enable Rails/SkipsModelValidations
channel = Channel::Whatsapp.find_by(account_id: @account.id)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Whatsapp")
end
def seed_sms_inbox
channel = Channel::Sms.create!(account: @account, phone_number: Faker::PhoneNumber.cell_phone_in_e164)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Mobile")
end
def seed_email_inbox
channel = Channel::Email.create!(account: @account, email: "test#{SecureRandom.hex}@#{@company_data['domain']}",
forward_to_email: "test_fwd#{SecureRandom.hex}@#{@company_data['domain']}")
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Email")
end
def seed_api_inbox
channel = Channel::Api.create!(account: @account)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} API")
end
def seed_telegram_inbox
# rubocop:disable Rails/SkipsModelValidations
bot_token = SecureRandom.hex
Channel::Telegram.insert(
{
account_id: @account.id,
bot_name: (@company_data['name']).to_s,
bot_token: bot_token,
created_at: Time.now.utc,
updated_at: Time.now.utc
},
returning: %w[id]
)
channel = Channel::Telegram.find_by(bot_token: bot_token)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Telegram")
# rubocop:enable Rails/SkipsModelValidations
end
def seed_line_inbox
channel = Channel::Line.create!(account: @account, line_channel_id: SecureRandom.hex, line_channel_secret: SecureRandom.hex,
line_channel_token: SecureRandom.hex)
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Line")
end
end

View File

@@ -0,0 +1,123 @@
module Seeders::MessageSeeder
def self.create_sample_email_collect_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content_type: :input_email,
content: 'Get notified by email'
)
end
def self.create_sample_csat_collect_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content_type: :input_csat,
content: 'Please rate the support'
)
end
def self.create_sample_cards_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content_type: 'cards',
content: 'cards',
content_attributes: {
items: [
sample_card_item,
sample_card_item
]
}
)
end
def self.sample_card_item
{
media_url: 'https://i.imgur.com/d8Djr4k.jpg',
title: 'Acme Shoes 2.0',
description: 'Move with Acme Shoe 2.0',
actions: [
{
type: 'link',
text: 'View More',
uri: 'http://acme-shoes.inc'
},
{
type: 'postback',
text: 'Add to cart',
payload: 'ITEM_SELECTED'
}
]
}
end
def self.create_sample_input_select_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content: 'Your favorite food',
content_type: 'input_select',
content_attributes: {
items: [
{ title: '🌯 Burito', value: 'Burito' },
{ title: '🍝 Pasta', value: 'Pasta' },
{ title: ' 🍱 Sushi', value: 'Sushi' },
{ title: ' 🥗 Salad', value: 'Salad' }
]
}
)
end
def self.create_sample_form_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content_type: 'form',
content: 'form',
content_attributes: sample_form
)
end
def self.sample_form
{
items: [
{ name: 'email', placeholder: 'Please enter your email', type: 'email', label: 'Email', required: 'required',
pattern_error: 'Please fill this field', pattern: '^[^\s@]+@[^\s@]+\.[^\s@]+$' },
{ name: 'text_area', placeholder: 'Please enter text', type: 'text_area', label: 'Large Text', required: 'required',
pattern_error: 'Please fill this field' },
{ name: 'text', placeholder: 'Please enter text', type: 'text', label: 'text', default: 'defaut value', required: 'required',
pattern: '^[a-zA-Z ]*$', pattern_error: 'Only alphabets are allowed' },
{ name: 'select', label: 'Select Option', type: 'select', options: [{ label: '🌯 Burito', value: 'Burito' },
{ label: '🍝 Pasta', value: 'Pasta' }] }
]
}
end
def self.create_sample_articles_message(conversation)
Message.create!(
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content: 'Tech Companies',
content_type: 'article',
content_attributes: {
items: [
{ title: 'Acme Hardware', description: 'Hardware reimagined', link: 'http://acme-hardware.inc' },
{ title: 'Acme Search', description: 'The best Search Engine', link: 'http://acme-search.inc' }
]
}
)
end
end

View File

@@ -0,0 +1,119 @@
# frozen_string_literal: true
require 'faker'
require 'active_support/testing/time_helpers'
class Seeders::Reports::ConversationCreator
include ActiveSupport::Testing::TimeHelpers
def initialize(account:, resources:)
@account = account
@contacts = resources[:contacts]
@inboxes = resources[:inboxes]
@teams = resources[:teams]
@labels = resources[:labels]
@agents = resources[:agents]
@priorities = [nil, 'urgent', 'high', 'medium', 'low']
end
# rubocop:disable Metrics/MethodLength
def create_conversation(created_at:)
conversation = nil
should_resolve = false
resolution_time = nil
ActiveRecord::Base.transaction do
travel_to(created_at) do
conversation = build_conversation
conversation.save!
add_labels_to_conversation(conversation)
create_messages_for_conversation(conversation)
# Determine if should resolve but don't update yet
should_resolve = rand > 0.3
if should_resolve
resolution_delay = rand((30.minutes)..(24.hours))
resolution_time = created_at + resolution_delay
end
end
travel_back
end
# Now resolve outside of time travel if needed
if should_resolve && resolution_time
# rubocop:disable Rails/SkipsModelValidations
conversation.update_column(:status, :resolved)
conversation.update_column(:updated_at, resolution_time)
# rubocop:enable Rails/SkipsModelValidations
# Trigger the event with proper timestamp
travel_to(resolution_time) do
trigger_conversation_resolved_event(conversation)
end
travel_back
end
conversation
end
# rubocop:enable Metrics/MethodLength
private
def build_conversation
contact = @contacts.sample
inbox = @inboxes.sample
contact_inbox = find_or_create_contact_inbox(contact, inbox)
assignee = select_assignee(inbox)
team = select_team
priority = @priorities.sample
contact_inbox.conversations.new(
account: @account,
inbox: inbox,
contact: contact,
assignee: assignee,
team: team,
priority: priority
)
end
def find_or_create_contact_inbox(contact, inbox)
inbox.contact_inboxes.find_or_create_by!(
contact: contact,
source_id: SecureRandom.hex
)
end
def select_assignee(inbox)
rand(10) < 8 ? inbox.members.sample : nil
end
def select_team
rand(10) < 7 ? @teams.sample : nil
end
def add_labels_to_conversation(conversation)
labels_to_add = @labels.sample(rand(5..20))
conversation.update_labels(labels_to_add.map(&:title))
end
def create_messages_for_conversation(conversation)
message_creator = Seeders::Reports::MessageCreator.new(
account: @account,
agents: @agents,
conversation: conversation
)
message_creator.create_messages
end
def trigger_conversation_resolved_event(conversation)
event_data = { conversation: conversation }
ReportingEventListener.instance.conversation_resolved(
Events::Base.new('conversation_resolved', Time.current, event_data)
)
end
end

View File

@@ -0,0 +1,141 @@
# frozen_string_literal: true
require 'faker'
require 'active_support/testing/time_helpers'
class Seeders::Reports::MessageCreator
include ActiveSupport::Testing::TimeHelpers
MESSAGES_PER_CONVERSATION = 5
def initialize(account:, agents:, conversation:)
@account = account
@agents = agents
@conversation = conversation
end
def create_messages
message_count = rand(MESSAGES_PER_CONVERSATION..MESSAGES_PER_CONVERSATION + 5)
first_agent_reply = true
message_count.times do |i|
message = create_single_message(i)
first_agent_reply = handle_reply_tracking(message, i, first_agent_reply)
end
end
def create_single_message(index)
is_incoming = index.even?
add_realistic_delay(index, is_incoming) if index.positive?
create_message(is_incoming)
end
def handle_reply_tracking(message, index, first_agent_reply)
return first_agent_reply if index.even? # Skip incoming messages
handle_agent_reply_events(message, first_agent_reply)
false # No longer first reply after any agent message
end
private
def add_realistic_delay(_message_index, is_incoming)
delay = calculate_message_delay(is_incoming)
travel(delay)
end
def calculate_message_delay(is_incoming)
if is_incoming
# Customer response time: 1 minute to 4 hours
rand((1.minute)..(4.hours))
elsif business_hours_active?(Time.current)
# Agent response time varies by business hours
rand((30.seconds)..(30.minutes))
else
rand((1.hour)..(8.hours))
end
end
def create_message(is_incoming)
if is_incoming
create_incoming_message
else
create_outgoing_message
end
end
def create_incoming_message
@conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :incoming,
content: generate_message_content,
sender: @conversation.contact
)
end
def create_outgoing_message
sender = @conversation.assignee || @agents.sample
@conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :outgoing,
content: generate_message_content,
sender: sender
)
end
def generate_message_content
Faker::Lorem.paragraph(sentence_count: rand(1..5))
end
def handle_agent_reply_events(message, is_first_reply)
if is_first_reply
trigger_first_reply_event(message)
else
trigger_reply_event(message)
end
end
def business_hours_active?(time)
weekday = time.wday
hour = time.hour
weekday.between?(1, 5) && hour.between?(9, 17)
end
def trigger_first_reply_event(message)
event_data = {
message: message,
conversation: message.conversation
}
ReportingEventListener.instance.first_reply_created(
Events::Base.new('first_reply_created', Time.current, event_data)
)
end
def trigger_reply_event(message)
waiting_since = calculate_waiting_since(message)
event_data = {
message: message,
conversation: message.conversation,
waiting_since: waiting_since
}
ReportingEventListener.instance.reply_created(
Events::Base.new('reply_created', Time.current, event_data)
)
end
def calculate_waiting_since(message)
last_customer_message = message.conversation.messages
.where(message_type: :incoming)
.where('created_at < ?', message.created_at)
.order(:created_at)
.last
last_customer_message&.created_at || message.conversation.created_at
end
end

View File

@@ -0,0 +1,234 @@
# frozen_string_literal: true
# Reports Data Seeder
#
# Generates realistic test data for performance testing of reports and analytics.
# Creates conversations, messages, contacts, agents, teams, and labels with proper
# reporting events (first response times, resolution times, etc.) using time travel
# to generate historical data with realistic timestamps.
#
# Usage:
# ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
#
# This will create:
# - 1000 conversations with realistic message exchanges
# - 100 contacts with realistic profiles
# - 20 agents assigned to teams and inboxes
# - 5 teams with realistic distribution
# - 30 labels with random assignments
# - 3 inboxes with agent assignments
# - Realistic reporting events with historical timestamps
#
# Note: This seeder clears existing data for the account before seeding.
require 'faker'
require_relative 'conversation_creator'
require_relative 'message_creator'
# rubocop:disable Rails/Output
class Seeders::Reports::ReportDataSeeder
include ActiveSupport::Testing::TimeHelpers
TOTAL_CONVERSATIONS = 1000
TOTAL_CONTACTS = 100
TOTAL_AGENTS = 20
TOTAL_TEAMS = 5
TOTAL_LABELS = 30
TOTAL_INBOXES = 3
MESSAGES_PER_CONVERSATION = 5
START_DATE = 3.months.ago # rubocop:disable Rails/RelativeDateConstant
END_DATE = Time.current
def initialize(account:)
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
@account = account
@teams = []
@agents = []
@labels = []
@inboxes = []
@contacts = []
end
def perform!
puts "Starting reports data seeding for account: #{@account.name}"
# Clear existing data
clear_existing_data
create_teams
create_agents
create_labels
create_inboxes
create_contacts
create_conversations
puts "Completed reports data seeding for account: #{@account.name}"
end
private
def clear_existing_data
puts "Clearing existing data for account: #{@account.id}"
@account.teams.destroy_all
@account.conversations.destroy_all
@account.labels.destroy_all
@account.inboxes.destroy_all
@account.contacts.destroy_all
@account.agents.destroy_all
@account.reporting_events.destroy_all
end
def create_teams
TOTAL_TEAMS.times do |i|
team = @account.teams.create!(
name: "#{Faker::Company.industry} Team #{i + 1}"
)
@teams << team
print "\rCreating teams: #{i + 1}/#{TOTAL_TEAMS}"
end
print "\n"
end
def create_agents
TOTAL_AGENTS.times do |i|
user = create_single_agent(i)
assign_agent_to_teams(user)
@agents << user
print "\rCreating agents: #{i + 1}/#{TOTAL_AGENTS}"
end
print "\n"
end
def create_single_agent(index)
random_suffix = SecureRandom.hex(4)
user = User.create!(
name: Faker::Name.name,
email: "agent_#{index + 1}_#{random_suffix}@#{@account.domain || 'example.com'}",
password: 'Password1!.',
confirmed_at: Time.current
)
user.skip_confirmation!
user.save!
AccountUser.create!(
account_id: @account.id,
user_id: user.id,
role: :agent
)
user
end
def assign_agent_to_teams(user)
teams_to_assign = @teams.sample(rand(1..3))
teams_to_assign.each do |team|
TeamMember.create!(
team_id: team.id,
user_id: user.id
)
end
end
def create_labels
TOTAL_LABELS.times do |i|
label = @account.labels.create!(
title: "Label-#{i + 1}-#{Faker::Lorem.word}",
description: Faker::Company.catch_phrase,
color: Faker::Color.hex_color
)
@labels << label
print "\rCreating labels: #{i + 1}/#{TOTAL_LABELS}"
end
print "\n"
end
def create_inboxes
TOTAL_INBOXES.times do |_i|
inbox = create_single_inbox
assign_agents_to_inbox(inbox)
@inboxes << inbox
print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
end
print "\n"
end
def create_single_inbox
channel = Channel::WebWidget.create!(
website_url: "https://#{Faker::Internet.domain_name}",
account_id: @account.id
)
@account.inboxes.create!(
name: "#{Faker::Company.name} Website",
channel: channel
)
end
def assign_agents_to_inbox(inbox)
agents_to_assign = if @inboxes.empty?
# First inbox gets all agents to ensure coverage
@agents
else
# Subsequent inboxes get random selection with some overlap
min_agents = [@agents.size / TOTAL_INBOXES, 10].max
max_agents = [(@agents.size * 0.8).to_i, 50].min
@agents.sample(rand(min_agents..max_agents))
end
agents_to_assign.each do |agent|
InboxMember.create!(inbox: inbox, user: agent)
end
end
def create_contacts
TOTAL_CONTACTS.times do |i|
contact = @account.contacts.create!(
name: Faker::Name.name,
email: Faker::Internet.email,
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
identifier: SecureRandom.uuid,
additional_attributes: {
company: Faker::Company.name,
city: Faker::Address.city,
country: Faker::Address.country,
customer_since: Faker::Date.between(from: 2.years.ago, to: Time.zone.today)
}
)
@contacts << contact
print "\rCreating contacts: #{i + 1}/#{TOTAL_CONTACTS}"
end
print "\n"
end
def create_conversations
conversation_creator = Seeders::Reports::ConversationCreator.new(
account: @account,
resources: {
contacts: @contacts,
inboxes: @inboxes,
teams: @teams,
labels: @labels,
agents: @agents
}
)
TOTAL_CONVERSATIONS.times do |i|
created_at = Faker::Time.between(from: START_DATE, to: END_DATE)
conversation_creator.create_conversation(created_at: created_at)
completion_percentage = ((i + 1).to_f / TOTAL_CONVERSATIONS * 100).round
print "\rCreating conversations: #{i + 1}/#{TOTAL_CONVERSATIONS} (#{completion_percentage}%)"
end
print "\n"
end
end
# rubocop:enable Rails/Output

View File

@@ -0,0 +1,480 @@
company:
name: 'PaperLayer'
domain: 'paperlayer.test'
users:
- name: 'Michael Scott'
gender: male
email: 'michael_scott@paperlayer.test'
team:
- 'sales'
- 'management'
- 'administration'
- 'warehouse'
role: 'administrator'
- name: 'David Wallace'
gender: male
email: 'david@paperlayer.test'
team:
- 'Management'
- name: 'Deangelo Vickers'
gender: male
email: 'deangelo@paperlayer.test'
team:
- 'Management'
- name: 'Jo Bennett'
gender: female
email: 'jo@paperlayer.test'
team:
- 'Management'
- name: 'Josh Porter'
gender: male
email: 'josh@paperlayer.test'
team:
- 'Management'
- name: 'Charles Miner'
gender: male
email: 'charles@paperlayer.test'
team:
- 'Management'
- name: 'Ed Truck'
gender: male
email: 'ed@paperlayer.test'
team:
- 'Management'
- name: 'Dan Gore'
gender: male
email: 'dan@paperlayer.test'
team:
- 'Management'
- name: 'Craig D'
gender: male
email: 'craig@paperlayer.test'
team:
- 'Management'
- name: 'Troy Underbridge'
gender: male
email: 'troy@paperlayer.test'
team:
- 'Management'
- name: 'Karen Filippelli'
gender: female
email: 'karn@paperlayer.test'
team:
- 'Sales'
custom_role: 'Sales Representative'
- name: 'Danny Cordray'
gender: female
email: 'danny@paperlayer.test'
team:
- 'Sales'
custom_role: 'Customer Support Lead'
- name: 'Ben Nugent'
gender: male
email: 'ben@paperlayer.test'
team:
- 'Sales'
custom_role: 'Junior Agent'
- name: 'Todd Packer'
gender: male
email: 'todd@paperlayer.test'
team:
- 'Sales'
custom_role: 'Sales Representative'
- name: 'Cathy Simms'
gender: female
email: 'cathy@paperlayer.test'
team:
- 'Administration'
custom_role: 'Knowledge Manager'
- name: 'Hunter Jo'
gender: male
email: 'hunter@paperlayer.test'
team:
- 'Administration'
custom_role: 'Analytics Specialist'
- name: 'Rolando Silva'
gender: male
email: 'rolando@paperlayer.test'
team:
- 'Administration'
custom_role: 'Junior Agent'
- name: 'Stephanie Wilson'
gender: female
email: 'stephanie@paperlayer.test'
team:
- 'Administration'
custom_role: 'Escalation Handler'
- name: 'Jordan Garfield'
gender: male
email: 'jorodan@paperlayer.test'
team:
- 'Administration'
- name: 'Ronni Carlo'
gender: male
email: 'ronni@paperlayer.test'
team:
- 'Administration'
- name: 'Lonny Collins'
gender: female
email: 'lonny@paperlayer.test'
team:
- 'Warehouse'
custom_role: 'Customer Support Lead'
- name: 'Madge Madsen'
gender: female
email: 'madge@paperlayer.test'
team:
- 'Warehouse'
- name: 'Glenn Max'
gender: female
email: 'glenn@paperlayer.test'
team:
- 'Warehouse'
- name: 'Jerry DiCanio'
gender: male
email: 'jerry@paperlayer.test'
team:
- 'Warehouse'
- name: 'Phillip Martin'
gender: male
email: 'phillip@paperlayer.test'
team:
- 'Warehouse'
- name: 'Michael Josh'
gender: male
email: 'michale_josh@paperlayer.test'
team:
- 'Warehouse'
- name: 'Matt Hudson'
gender: male
email: 'matt@paperlayer.test'
team:
- 'Warehouse'
- name: 'Gideon'
gender: male
email: 'gideon@paperlayer.test'
team:
- 'Warehouse'
- name: 'Bruce'
gender: male
email: 'bruce@paperlayer.test'
team:
- 'Warehouse'
- name: 'Frank'
gender: male
email: 'frank@paperlayer.test'
team:
- 'Warehouse'
- name: 'Louanne Kelley'
gender: female
email: 'louanne@paperlayer.test'
- name: 'Devon White'
gender: male
email: 'devon@paperlayer.test'
custom_role: 'Escalation Handler'
- name: 'Kendall'
gender: male
email: 'kendall@paperlayer.test'
- email: 'sadiq@paperlayer.test'
name: 'Sadiq'
gender: male
teams:
- '💰 Sales'
- '💼 Management'
- '👩‍💼 Administration'
- '🚛 Warehouse'
custom_roles:
- name: 'Customer Support Lead'
description: 'Lead support agent with full conversation and contact management'
permissions:
- 'conversation_manage'
- 'contact_manage'
- 'report_manage'
- name: 'Sales Representative'
description: 'Sales team member with conversation and contact access'
permissions:
- 'conversation_unassigned_manage'
- 'conversation_participating_manage'
- 'contact_manage'
- name: 'Knowledge Manager'
description: 'Manages knowledge base and participates in conversations'
permissions:
- 'knowledge_base_manage'
- 'conversation_participating_manage'
- name: 'Junior Agent'
description: 'Entry-level agent with basic conversation access'
permissions:
- 'conversation_participating_manage'
- name: 'Analytics Specialist'
description: 'Focused on reports and data analysis'
permissions:
- 'report_manage'
- 'conversation_participating_manage'
- name: 'Escalation Handler'
description: 'Handles unassigned conversations and escalations'
permissions:
- 'conversation_unassigned_manage'
- 'conversation_participating_manage'
- 'contact_manage'
labels:
- title: 'billing'
color: '#28AD21'
show_on_sidebar: true
- title: 'software'
color: '#8F6EF2'
show_on_sidebar: true
- title: 'delivery'
color: '#A2FDD5'
show_on_sidebar: true
- title: 'ops-handover'
color: '#A53326'
show_on_sidebar: true
- title: 'premium-customer'
color: '#6FD4EF'
show_on_sidebar: true
- title: 'lead'
color: '#F161C8'
show_on_sidebar: true
contacts:
- name: "Lorrie Trosdall"
email: "ltrosdall0@bravesites.test"
gender: 'female'
conversations:
- channel: Channel::WebWidget
messages:
- message_type: incoming
content: Hi, I'm having trouble logging in to my account.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Hi! Sorry to hear that. Can you please provide me with your username and email address so I can look into it for you?
- name: "Tiffanie Cloughton"
email: "tcloughton1@newyorker.test"
gender: 'female'
conversations:
- channel: Channel::FacebookPage
messages:
- message_type: incoming
content: Hi, I need some help with my billing statement.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Hello! I'd be happy to assist you with that. Can you please tell me which billing statement you're referring to?
- name: "Melonie Keatch"
email: "mkeatch2@reuters.test"
gender: 'female'
conversations:
- channel: Channel::TwitterProfile
messages:
- message_type: incoming
content: Hi, I think I accidentally deleted some important files. Can you help me recover them?
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Of course! Can you please tell me what type of files they were and where they were located on your device?
- name: "Olin Canniffe"
email: "ocanniffe3@feedburner.test"
gender: 'male'
conversations:
- channel: Channel::Whatsapp
source_id: "123456723"
messages:
- message_type: incoming
content: Hi, I'm having trouble connecting to the internet.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Have you tried restarting your modem/router? If that doesn't work, please let me know and I can provide further assistance.
- name: "Viviene Corp"
email: "vcorp4@instagram.test"
gender: 'female'
conversations:
- channel: Channel::Sms
source_id: "+1234567"
messages:
- message_type: incoming
content: Hi, I'm having trouble with the mobile app. It keeps crashing.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Can you please try uninstalling and reinstalling the app and see if that helps? If not, please let me know and I can look into it further.
- name: "Drake Pittway"
email: "dpittway5@chron.test"
gender: 'male'
conversations:
- channel: Channel::Line
messages:
- message_type: incoming
content: Hi, I'm trying to update my account information but it won't save.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Sorry for the inconvenience. Can you please provide me with the specific information you're trying to update and the error message you're receiving?
- name: "Klaus Crawley"
email: "kcrawley6@narod.ru"
gender: 'male'
conversations:
- channel: Channel::WebWidget
messages:
- message_type: incoming
content: Hi, I need some help setting up my new device.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: No problem! Can you please tell me the make and model of your device and what specifically you need help with?
- name: "Bing Cusworth"
email: "bcusworth7@arstechnica.test"
gender: 'male'
conversations:
- channel: Channel::TwitterProfile
messages:
- message_type: incoming
content: Hi, I accidentally placed an order for the wrong item. Can I cancel it?
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Can you please provide me with your order number and I'll see if I can cancel it for you?
- name: "Claus Jira"
email: "cjira8@comcast.net"
gender: 'male'
conversations:
- channel: Channel::Whatsapp
source_id: "12323432"
messages:
- message_type: incoming
content: Hi, I'm having trouble with my email. I can't seem to send or receive any messages.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Can you please tell me what email client you're using and if you're receiving any error messages?
- name: "Quent Dalliston"
email: "qdalliston9@zimbio.test"
gender: 'male'
conversations:
- channel: Channel::Whatsapp
source_id: "12342234324"
messages:
- message_type: incoming
content: Hi, I need some help resetting my password.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Sure! Can you please provide me with your username or email address and I'll send you a password reset link?
- name: "Coreen Mewett"
email: "cmewetta@home.pl"
gender: 'female'
conversations:
- channel: Channel::FacebookPage
messages:
- message_type: incoming
content: Hi, I think someone may have hacked into my account. What should I do?
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Please change your password immediately and enable two-factor authentication if you haven't already done so. I can also assist you in reviewing your account activity if needed.
- name: "Benyamin Janeway"
email: "bjanewayb@ustream.tv"
gender: 'male'
conversations:
- channel: Channel::Line
messages:
- message_type: incoming
content: Hi, I have a question about your product features.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Sure thing! What specific feature are you interested in learning more about?
- name: "Cordell Dalinder"
email: "cdalinderc@msn.test"
gender: 'male'
conversations:
- channel: Channel::Email
source_id: "cdalinderc@msn.test"
messages:
- message_type: incoming
content: Hi, I need help setting up my new printer.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: No problem! Can you please provide me with the make and model of your printer and what type of device you'll be connecting it to?
- name: "Merrile Petruk"
email: "mpetrukd@wunderground.test"
gender: 'female'
conversations:
- channel: Channel::Email
source_id: "mpetrukd@wunderground.test"
priority: urgent
messages:
- message_type: incoming
content: Hi, I'm having trouble accessing a file that I shared with someone.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: I'm sorry to hear that. Can you please tell me which file you're having trouble accessing and who you shared it with? I'll do my best to help you regain access.
- name: "Nathaniel Vannuchi"
email: "nvannuchie@photobucket.test"
gender: 'male'
conversations:
- channel: Channel::FacebookPage
priority: high
messages:
- message_type: incoming
content: "Hey there,I need some help with billing, my card is not working on the website."
- name: "Olia Olenchenko"
email: "oolenchenkof@bluehost.test"
gender: 'female'
conversations:
- channel: Channel::WebWidget
priority: high
assignee: michael_scott@paperlayer.test
messages:
- message_type: incoming
content: "Billing section is not working, it throws some error."
- name: "Elisabeth Derington"
email: "ederingtong@printfriendly.test"
gender: 'female'
conversations:
- channel: Channel::Whatsapp
priority: high
source_id: "1223423567"
labels:
- billing
- delivery
- ops-handover
- premium-customer
messages:
- message_type: incoming
content: "Hey \n I didn't get the product delivered, but it shows it is delivered to my address. Please check"
- name: "Willy Castelot"
email: "wcasteloth@exblog.jp"
gender: 'male'
conversations:
- channel: Channel::WebWidget
priority: medium
labels:
- software
- ops-handover
messages:
- message_type: incoming
content: "Hey there, \n I need some help with the product, my button is not working on the website."
- name: "Ophelia Folkard"
email: "ofolkardi@taobao.test"
gender: 'female'
conversations:
- channel: Channel::WebWidget
priority: low
assignee: michael_scott@paperlayer.test
labels:
- billing
- software
- lead
messages:
- message_type: incoming
content: "Hey, \n My card is not working on your website. Please help"
- name: "Candice Matherson"
email: "cmathersonj@va.test"
gender: 'female'
conversations:
- channel: Channel::Email
priority: urgent
source_id: "cmathersonj@va.test"
assignee: michael_scott@paperlayer.test
labels:
- billing
- lead
messages:
- message_type: incoming
content: "Hey, \n I'm looking for some help to figure out if it is the right product for me."
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: Welcome to PaperLayer. Our Team will be getting back you shortly.
- message_type: outgoing
sender: michael_scott@paperlayer.test
content: How may i help you ?
sender: michael_scott@paperlayer.test