Compare commits

..
Author SHA1 Message Date
iamsivin c6a8ca8338 Merge branch 'develop' into feat/reports-filter 2026-02-06 18:26:11 +05:30
iamsivin da220b6fcb fix: improve date range formatting in date picker button 2026-02-06 18:03:23 +05:30
Shivam MishraandGitHub e9d7c4d5dc Merge branch 'develop' into feat/reports-filter 2026-02-06 17:27:21 +05:30
Shivam Mishra a9ee7819b3 feat: apply filter on click outside 2026-02-06 17:25:14 +05:30
Shivam Mishra 2456ef2729 fix: button 2026-02-06 17:19:13 +05:30
Shivam Mishra 59a44add5c chore: add todo 2026-02-06 17:15:29 +05:30
Shivam Mishra a37bcada72 fix: recalculate navigation offset after URL date restore
Arrow navigation was relative to current period instead of the
restored range. Now recomputes monthOffset from the difference
between restored dates and current period on URL restore.
2026-02-06 17:10:47 +05:30
Shivam Mishra 7a1a9fbd9b fix: use correct property names from getActiveDateRange
getActiveDateRange returns { start, end } not { startDate, endDate }.
Also pass currentDate to avoid undefined date calculations.
2026-02-06 16:45:18 +05:30
Shivam Mishra 122df392e0 fix: resolve date picker navigation bugs
- Reset monthOffset when selecting custom dates to prevent stale state
- Use date-based comparison for canNavigateNext instead of monthOffset
  so forward navigation works correctly after URL restore
- Use date-based comparison for week navigation label instead of
  monthOffset so "Week #N" label displays correctly after URL restore
2026-02-06 16:44:48 +05:30
Shivam Mishra 338b48d3c5 feat: add separator 2026-02-06 16:34:34 +05:30
Shivam Mishra b0be108ea0 feat: better year formatting 2026-02-06 16:34:27 +05:30
Shivam Mishra 402ffce895 feat: add this month and this week to date picker 2026-02-06 16:22:52 +05:30
Shivam Mishra 3fc9d78025 fix: height 2026-02-06 14:11:17 +05:30
iamsivin 3691f7b8b9 feat: Add URL persistence for SLA and CSAT report filters 2026-02-05 13:38:58 +05:30
iamsivin c52bf3571b feat: Add URL persistence for report filters 2026-02-05 01:55:43 +05:30
Sivin VargheseandGitHub 8ab0328b15 Merge branch 'develop' into feat/reports-filter 2026-02-04 20:47:57 +05:30
iamsivin 6cc9cce327 feat: Refactor reports filters 2026-02-04 16:14:23 +05:30
17 changed files with 495 additions and 2320 deletions
+7 -7
View File
@@ -144,7 +144,7 @@ jobs:
# Backend tests with parallelization
backend-tests:
<<: *defaults
parallelism: 18
parallelism: 20
steps:
- checkout
- node/install:
@@ -350,12 +350,12 @@ jobs:
destination: coverage
build:
<<: *defaults
steps:
- run:
name: Legacy build aggregator
command: |
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
<<: *defaults
steps:
- run:
name: Legacy build aggregator
command: |
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
workflows:
version: 2
@@ -11,13 +11,6 @@ import {
} from '../helpers/reportFilterHelper';
import { DATE_RANGE_TYPES } from 'dashboard/components/ui/DatePicker/helpers/DatePickerHelper';
defineProps({
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['filterChange']);
const route = useRoute();
@@ -86,10 +79,7 @@ onMounted(() => {
</script>
<template>
<div
class="flex flex-col justify-between gap-3 md:flex-row"
:class="{ 'pointer-events-none opacity-50': disabled }"
>
<div class="flex flex-col justify-between gap-3 md:flex-row">
<div class="flex flex-col flex-wrap items-start gap-2 md:flex-row">
<WootDatePicker
v-model:date-range="customDateRange"
@@ -1,9 +1,7 @@
<script setup>
import OverviewReportFilters from './OverviewReportFilters.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import { formatTime } from '@chatwoot/utils';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import Table from 'dashboard/components/table/Table.vue';
import { generateFileName } from 'dashboard/helper/downloadHelper';
import {
@@ -44,16 +42,6 @@ const businessHours = ref(false);
import { useI18n } from 'vue-i18n';
import SummaryReportLink from './SummaryReportLink.vue';
const flagMap = {
agent: 'isFetchingAgentSummaryReports',
inbox: 'isFetchingInboxSummaryReports',
team: 'isFetchingTeamSummaryReports',
label: 'isFetchingLabelSummaryReports',
};
const uiFlags = useMapGetter('summaryReports/getUIFlags');
const isLoading = computed(() => uiFlags.value[flagMap[props.type]] ?? false);
const rowItems = useMapGetter([props.getterKey]) || [];
const reportMetrics = useMapGetter([props.summaryKey]) || [];
@@ -132,26 +120,13 @@ const tableData = computed(() =>
})
);
const fetchReportsWithRetry = async () => {
const params = {
const fetchAllData = () => {
store.dispatch(props.fetchItemsKey);
store.dispatch(props.actionKey, {
since: from.value,
until: to.value,
businessHours: businessHours.value,
};
try {
await store.dispatch(props.actionKey, params);
} catch {
try {
await store.dispatch(props.actionKey, params);
} catch {
useAlert(t('REPORT.SUMMARY_FETCHING_FAILED'));
}
}
};
const fetchAllData = () => {
store.dispatch(props.fetchItemsKey);
fetchReportsWithRetry();
});
};
onMounted(() => fetchAllData());
@@ -203,28 +178,10 @@ defineExpose({ downloadReports });
</script>
<template>
<OverviewReportFilters
:disabled="isLoading"
@filter-change="onFilterChange"
/>
<OverviewReportFilters @filter-change="onFilterChange" />
<div
class="relative flex-1 overflow-auto px-2 py-2 mt-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
class="flex-1 overflow-auto px-2 py-2 mt-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
>
<Table :table="table" />
<Transition
enter-active-class="transition-opacity duration-300 ease-out"
leave-active-class="transition-opacity duration-200 ease-in"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="isLoading"
class="absolute inset-0 flex justify-center pt-[12.5rem] bg-n-solid-1/70 rounded-xl pointer-events-none"
>
<Spinner :size="32" class="text-n-brand" />
</div>
</Transition>
</div>
</template>
@@ -141,14 +141,12 @@ describe('Summary Reports Store', () => {
});
});
it('should reset uiFlags and rethrow error on failure', async () => {
it('should handle errors gracefully', async () => {
SummaryReportsAPI.getInboxReports.mockRejectedValue(
new Error('API Error')
);
await expect(
store.actions.fetchInboxSummaryReports({ commit }, {})
).rejects.toThrow('API Error');
await store.actions.fetchInboxSummaryReports({ commit }, {});
expect(commit).toHaveBeenCalledWith('setUIFlags', {
isFetchingInboxSummaryReports: false,
@@ -28,17 +28,15 @@ async function fetchSummaryReports(type, params, { commit }) {
const config = typeMap[type];
if (!config) return;
let error = null;
try {
commit('setUIFlags', { [config.flagKey]: true });
const response = await SummaryReportsAPI[config.apiMethod](params);
commit(config.mutationKey, camelcaseKeys(response.data, { deep: true }));
} catch (e) {
error = e;
} catch (error) {
// Ignore error
} finally {
commit('setUIFlags', { [config.flagKey]: false });
}
if (error) throw error;
}
export const initialState = {
+1 -2
View File
@@ -13,8 +13,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
end
rescue *ExceptionList::IMAP_EXCEPTIONS => e
Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}"
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError,
Net::IMAP::ResponseParseError, Net::IMAP::ResponseReadError, Net::IMAP::ResponseTooLargeError => e
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError => e
Rails.logger.error "Error for email channel - #{channel.inbox.id} : #{e.message}"
rescue LockAcquisitionError
Rails.logger.error "Lock failed for #{channel.inbox.id}"
-13
View File
@@ -2,19 +2,6 @@ class Internal::SeedAccountJob < ApplicationJob
queue_as :low
def perform(account)
# Reload account to ensure we have fresh data
account.reload
# Perform seeding
Seeders::AccountSeeder.new(account: account).perform!
# Reload again after seeding to ensure cache is fresh
account.reload
Rails.logger.info("Account seeding completed successfully for account_id: #{account.id}")
rescue StandardError => e
Rails.logger.error("Account seeding failed for account_id: #{account.id} - #{e.message}")
Rails.logger.error(e.backtrace.join("\n"))
raise
end
end
+7 -9
View File
@@ -1,6 +1,4 @@
class AccountDeletionService
SOFT_DELETE_EMAIL_DOMAIN = '@chatwoot-deleted.invalid'.freeze
attr_reader :account, :soft_deleted_users
def initialize(account:)
@@ -27,11 +25,15 @@ class AccountDeletionService
def soft_delete_orphaned_users
account.users.each do |user|
# Skip users who are still associated with another account.
next if user.account_users.where.not(account_id: account.id).exists?
# Find all account_users for this user excluding the current account
other_accounts = user.account_users.where.not(account_id: account.id).count
# If user has no other accounts, soft delete them
next unless other_accounts.zero?
# Soft delete user by appending -deleted.com to email
original_email = user.email
user.email = soft_deleted_email_for(user)
user.email = "#{original_email}-deleted.com"
user.skip_reconfirmation!
user.save!
@@ -45,8 +47,4 @@ class AccountDeletionService
Rails.logger.info("Soft deleted user #{user.id} with email #{original_email}")
end
end
def soft_deleted_email_for(user)
"#{user.id}#{SOFT_DELETE_EMAIL_DOMAIN}"
end
end
-3
View File
@@ -78,11 +78,8 @@ class ActionService
emails = emails[0].gsub(/\s+/, '').split(',')
emails.each do |email|
break unless @account.within_email_rate_limit?
email = parse_email_variables(@conversation, email)
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
@account.increment_email_sent_count
end
end
@@ -58,10 +58,7 @@ class AutomationRules::ActionService < ActionService
teams = Team.where(id: params[0][:team_ids])
teams.each do |team|
break unless @account.within_email_rate_limit?
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[0][:message])&.deliver_now
@account.increment_email_sent_count
end
end
end
@@ -175,9 +175,6 @@ class Whatsapp::IncomingMessageBaseService
end
def create_message(message, source_id: nil)
content_attrs = outgoing_echo ? { external_echo: true } : {}
content_attrs[:in_reply_to_external_id] = @in_reply_to_external_id if @in_reply_to_external_id.present?
@message = @conversation.messages.build(
content: message_content(message),
account_id: @inbox.account_id,
@@ -187,7 +184,8 @@ class Whatsapp::IncomingMessageBaseService
status: outgoing_echo ? :delivered : :sent,
sender: outgoing_echo ? nil : @contact,
source_id: (source_id || message[:id]).to_s,
content_attributes: content_attrs
content_attributes: outgoing_echo ? { external_echo: true } : {},
in_reply_to_external_id: @in_reply_to_external_id
)
end
+45 -698
View File
@@ -18,733 +18,80 @@ unless Rails.env.production?
GlobalConfig.clear_cache
account = Account.create!(
name: 'Paperlayer'
name: 'Acme Inc'
)
secondary_account = Account.create!(
name: 'Acme Org'
)
# Create Admin
user = User.new(name: 'Sarah Johnson', email: 'sarah@paperlayer.com', password: 'Password1!', type: 'SuperAdmin')
user = User.new(name: 'John', email: 'john@acme.inc', password: 'Password1!', type: 'SuperAdmin')
user.skip_confirmation!
user.save!
# Create Sales Team
sales_lead = User.new(name: 'Marcus Chen', email: 'marcus@paperlayer.com', password: 'Password1!')
sales_lead.skip_confirmation!
sales_lead.save!
sales_agent_1 = User.new(name: 'Emily Rodriguez', email: 'emily@paperlayer.com', password: 'Password1!')
sales_agent_1.skip_confirmation!
sales_agent_1.save!
sales_agent_2 = User.new(name: 'David Kim', email: 'david@paperlayer.com', password: 'Password1!')
sales_agent_2.skip_confirmation!
sales_agent_2.save!
# Create Support Team
support_lead = User.new(name: 'Jennifer Williams', email: 'jennifer@paperlayer.com', password: 'Password1!')
support_lead.skip_confirmation!
support_lead.save!
support_agent_1 = User.new(name: 'Alex Thompson', email: 'alex@paperlayer.com', password: 'Password1!')
support_agent_1.skip_confirmation!
support_agent_1.save!
support_agent_2 = User.new(name: 'Rachel Green', email: 'rachel@paperlayer.com', password: 'Password1!')
support_agent_2.skip_confirmation!
support_agent_2.save!
# Create Operations Team
operations_lead = User.new(name: 'Tom Anderson', email: 'tom@paperlayer.com', password: 'Password1!')
operations_lead.skip_confirmation!
operations_lead.save!
operations_agent = User.new(name: 'Lisa Martinez', email: 'lisa@paperlayer.com', password: 'Password1!')
operations_agent.skip_confirmation!
operations_agent.save!
# Create Account Users
AccountUser.create!(account_id: account.id, user_id: user.id, role: :administrator)
AccountUser.create!(account_id: account.id, user_id: sales_lead.id, role: :administrator)
AccountUser.create!(account_id: account.id, user_id: sales_agent_1.id, role: :agent)
AccountUser.create!(account_id: account.id, user_id: sales_agent_2.id, role: :agent)
AccountUser.create!(account_id: account.id, user_id: support_lead.id, role: :administrator)
AccountUser.create!(account_id: account.id, user_id: support_agent_1.id, role: :agent)
AccountUser.create!(account_id: account.id, user_id: support_agent_2.id, role: :agent)
AccountUser.create!(account_id: account.id, user_id: operations_lead.id, role: :administrator)
AccountUser.create!(account_id: account.id, user_id: operations_agent.id, role: :agent)
AccountUser.create!(account_id: secondary_account.id, user_id: user.id, role: :administrator)
# Create Teams
sales_team = Team.create!(
account: account,
name: 'Sales Team',
description: 'Handles all sales inquiries, quotes, and bulk orders',
allow_auto_assign: true
AccountUser.create!(
account_id: account.id,
user_id: user.id,
role: :administrator
)
support_team = Team.create!(
account: account,
name: 'Customer Support',
description: 'Handles customer service, product questions, and order issues',
allow_auto_assign: true
AccountUser.create!(
account_id: secondary_account.id,
user_id: user.id,
role: :administrator
)
operations_team = Team.create!(
account: account,
name: 'Operations',
description: 'Handles shipping, logistics, and order fulfillment',
allow_auto_assign: true
)
web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc')
# Add members to Sales Team
TeamMember.create!(team: sales_team, user: sales_lead)
TeamMember.create!(team: sales_team, user: sales_agent_1)
TeamMember.create!(team: sales_team, user: sales_agent_2)
# Add members to Support Team
TeamMember.create!(team: support_team, user: support_lead)
TeamMember.create!(team: support_team, user: support_agent_1)
TeamMember.create!(team: support_team, user: support_agent_2)
# Add members to Operations Team
TeamMember.create!(team: operations_team, user: operations_lead)
TeamMember.create!(team: operations_team, user: operations_agent)
# Create Inboxes
web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://paperlayer.com')
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Website Chat')
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
InboxMember.create!(user: user, inbox: inbox)
InboxMember.create!(user: sales_lead, inbox: inbox)
InboxMember.create!(user: sales_agent_1, inbox: inbox)
InboxMember.create!(user: sales_agent_2, inbox: inbox)
InboxMember.create!(user: support_lead, inbox: inbox)
InboxMember.create!(user: support_agent_1, inbox: inbox)
InboxMember.create!(user: support_agent_2, inbox: inbox)
InboxMember.create!(user: operations_lead, inbox: inbox)
InboxMember.create!(user: operations_agent, inbox: inbox)
sales_inbox = Inbox.create!(channel: Channel::WebWidget.create!(account: account, website_url: 'https://paperlayer.com/sales'), account: account,
name: 'Sales Inquiries')
InboxMember.create!(user: user, inbox: sales_inbox)
InboxMember.create!(user: sales_lead, inbox: sales_inbox)
InboxMember.create!(user: sales_agent_1, inbox: sales_inbox)
InboxMember.create!(user: sales_agent_2, inbox: sales_inbox)
email_channel = Channel::Email.create!(
account: account,
email: 'support@paperlayer.com',
forward_to_email: 'support@paperlayer.com'
)
support_inbox = Inbox.create!(channel: email_channel, account: account, name: 'Email Support')
InboxMember.create!(user: support_lead, inbox: support_inbox)
InboxMember.create!(user: support_agent_1, inbox: support_inbox)
InboxMember.create!(user: support_agent_2, inbox: support_inbox)
# Create Labels
bulk_order_label = Label.create!(account: account, title: 'bulk-order', description: 'Large volume paper orders', color: '#00C875',
show_on_sidebar: true)
custom_print_label = Label.create!(account: account, title: 'custom-printing', description: 'Custom letterhead and printing requests',
color: '#784BD1', show_on_sidebar: true)
urgent_label = Label.create!(account: account, title: 'urgent', description: 'Rush delivery needed', color: '#E2445C', show_on_sidebar: true)
pricing_label = Label.create!(account: account, title: 'pricing-inquiry', description: 'Quote requests and pricing questions', color: '#FDAB3D',
show_on_sidebar: true)
delivery_label = Label.create!(account: account, title: 'delivery-issue', description: 'Shipping and delivery concerns', color: '#9CD326',
show_on_sidebar: true)
product_label = Label.create!(account: account, title: 'product-question', description: 'Questions about paper types and products',
color: '#579BFC', show_on_sidebar: true)
# Create Canned Responses
CannedResponse.create!(account: account, short_code: 'welcome',
content: 'Thank you for contacting Paperlayer! We\'re committed to providing the highest quality paper products for your business. How can I help you today?')
CannedResponse.create!(account: account, short_code: 'bulk',
content: 'For bulk orders over 100 reams, we offer competitive wholesale pricing. Let me get you a custom quote. Could you share your estimated monthly paper needs?')
CannedResponse.create!(account: account, short_code: 'pricing',
content: 'Our standard pricing for premium white copy paper (20lb, 8.5x11) is $45 per case (10 reams). We offer volume discounts starting at 50 cases.')
CannedResponse.create!(account: account, short_code: 'delivery',
content: 'We offer same-day delivery for local orders (within 50 miles) placed before noon, and 2-3 business days for standard shipping. Rush delivery is available for an additional fee.')
CannedResponse.create!(account: account, short_code: 'custom',
content: 'We provide custom letterhead printing services with a minimum order of 5 reams. Typical turnaround is 5-7 business days. Would you like to discuss your design requirements?')
CannedResponse.create!(account: account, short_code: 'stock',
content: 'We carry a full range of paper products: copy paper (20lb-32lb), cardstock, colored paper, glossy photo paper, and specialty papers. What type are you interested in?')
CannedResponse.create!(account: account, short_code: 'closing',
content: 'Thank you for choosing Paperlayer! Is there anything else I can help you with today?')
CannedResponse.create!(account: account, short_code: 'followup',
content: 'I wanted to follow up on your recent inquiry. Have you had a chance to review our quote?')
CannedResponse.create!(account: account, short_code: 'tracking',
content: 'I can help you track your order. Could you please provide your order number?')
CannedResponse.create!(account: account, short_code: 'samples',
content: 'We\'d be happy to send you paper samples! What types of paper are you interested in testing?')
# Create Macros
Macro.create!(
account: account,
name: 'Handle Bulk Order Inquiry',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'add_label', 'action_params' => [bulk_order_label.title] },
{ 'action_name' => 'assign_team', 'action_params' => [sales_team.id] },
{ 'action_name' => 'send_message',
'action_params' => ['For bulk orders over 100 reams, we offer competitive wholesale pricing. Let me get you a custom quote. Could you share your estimated monthly paper needs?'] }
]
)
Macro.create!(
account: account,
name: 'Handle Urgent Order',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'add_label', 'action_params' => [urgent_label.title] },
{ 'action_name' => 'change_priority', 'action_params' => ['urgent'] },
{ 'action_name' => 'assign_team', 'action_params' => [operations_team.id] },
{ 'action_name' => 'send_message',
'action_params' => ['I see you need this urgently. We offer same-day delivery for orders placed before noon. Let me check our inventory and get this expedited for you.'] }
]
)
Macro.create!(
account: account,
name: 'Handle Custom Printing Request',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'add_label', 'action_params' => [custom_print_label.title] },
{ 'action_name' => 'assign_team', 'action_params' => [support_team.id] },
{ 'action_name' => 'send_message',
'action_params' => ['We provide custom letterhead printing services with a minimum order of 5 reams. Typical turnaround is 5-7 business days. Would you like to discuss your design requirements?'] }
]
)
Macro.create!(
account: account,
name: 'Route to Sales Team',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'add_label', 'action_params' => [pricing_label.title] },
{ 'action_name' => 'assign_team', 'action_params' => [sales_team.id] },
{ 'action_name' => 'send_message',
'action_params' => ['I\'d be happy to provide pricing information. To give you the most accurate quote, could you let me know: 1) Paper type/weight you need, 2) Quantity required, 3) Delivery location?'] }
]
)
Macro.create!(
account: account,
name: 'Close with Satisfaction Check',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'send_message',
'action_params' => ['Thank you for choosing Paperlayer! Is there anything else I can help you with today?'] },
{ 'action_name' => 'resolve_conversation' }
]
)
Macro.create!(
account: account,
name: 'Escalate to Operations',
visibility: :global,
created_by: user,
actions: [
{ 'action_name' => 'add_label', 'action_params' => [delivery_label.title] },
{ 'action_name' => 'assign_team', 'action_params' => [operations_team.id] },
{ 'action_name' => 'change_priority', 'action_params' => ['high'] }
]
)
# ===============================================
# Create 50 Sample Conversations
# ===============================================
agents = [sales_lead, sales_agent_1, sales_agent_2, support_lead, support_agent_1, support_agent_2, operations_lead, operations_agent]
# Customer names for variety
customer_names = [
'Michael Stevens', 'Sarah Parker', 'John Martinez', 'Emily White', 'David Brown',
'Jennifer Taylor', 'Robert Wilson', 'Lisa Anderson', 'James Thomas', 'Mary Jackson',
'Christopher Lee', 'Patricia Harris', 'Daniel Clark', 'Linda Lewis', 'Matthew Walker',
'Barbara Hall', 'Joseph Allen', 'Nancy Young', 'Ryan King', 'Karen Wright',
'Kevin Lopez', 'Betty Hill', 'Brian Scott', 'Sandra Green', 'George Adams',
'Jessica Baker', 'Edward Nelson', 'Margaret Carter', 'Steven Mitchell', 'Helen Roberts',
'Andrew Turner', 'Dorothy Phillips', 'Mark Campbell', 'Carol Parker', 'Paul Evans',
'Michelle Edwards', 'Donald Collins', 'Ashley Stewart', 'Kenneth Morris', 'Kimberly Rogers',
'Joshua Reed', 'Amanda Cook', 'Jason Morgan', 'Melissa Bell', 'Justin Murphy',
'Stephanie Bailey', 'Brandon Rivera', 'Rebecca Cooper', 'Eric Richardson', 'Laura Cox'
]
companies = [
'Tech Innovations Inc', 'Global Enterprises', 'Summit Corporation', 'Bright Future LLC',
'Metro Solutions', 'Pacific Group', 'Alliance Partners', 'Premier Services',
'Apex Industries', 'Horizon Technologies', 'Fusion Consulting', 'Vista Corp',
'Pinnacle Systems', 'Catalyst Ventures', 'Quantum Labs', 'Nexus Holdings',
'Zenith Partners', 'Atlas Group', 'Infinity Solutions', 'Eclipse Enterprises'
]
conversation_templates = [
{
labels: [bulk_order_label.title, pricing_label.title],
team: sales_team,
priority: nil,
status: :open,
messages: [
{ role: :incoming, content: 'Hi, I need a quote for 200 cases of copy paper for our office. What kind of pricing can you offer?' },
{ role: :outgoing,
content: 'Great! For 200 cases, you qualify for our 15% volume discount. That brings the price down to $38.25 per case. Would you like me to prepare a formal quote?' },
{ role: :incoming, content: 'Yes please. How soon can you deliver?' }
]
},
{
labels: [custom_print_label.title],
team: support_team,
priority: nil,
status: :pending,
messages: [
{ role: :incoming, content: 'Do you offer custom letterhead printing? We need about 10 reams with our logo.' },
{ role: :outgoing,
content: 'Absolutely! We can do custom letterhead with a minimum order of 5 reams. For 10 reams on 28lb premium paper, the cost would be around $200. Turnaround is 5-7 business days. Do you have your design file ready?' }
]
},
{
labels: [urgent_label.title, delivery_label.title],
team: operations_team,
priority: :urgent,
status: :open,
messages: [
{ role: :incoming, content: 'Emergency! We need 50 reams delivered today. Is that possible?' },
{ role: :outgoing, content: 'Yes, we can do same-day delivery within 50 miles if you order before noon. What\'s your location?' },
{ role: :incoming, content: 'We\'re in downtown. How much for rush delivery?' }
]
},
{
labels: [product_label.title],
team: support_team,
priority: nil,
status: :resolved,
messages: [
{ role: :incoming, content: 'What\'s the difference between 20lb and 24lb paper?' },
{ role: :outgoing,
content: '20lb is our standard weight, great for everyday printing. 24lb is heavier, more professional feel, and less see-through. Perfect for presentations and client-facing documents.' },
{ role: :incoming, content: 'Perfect, I\'ll go with 24lb. Thanks!' }
]
},
{
labels: [pricing_label.title],
team: sales_team,
priority: nil,
status: :open,
messages: [
{ role: :incoming, content: 'Can you send me your current price list for all paper weights?' },
{ role: :outgoing,
content: 'Of course! I\'ll email you our comprehensive price list. Are you interested in standard sizes or do you need custom dimensions as well?' }
]
},
{
labels: [delivery_label.title],
team: operations_team,
priority: :high,
status: :pending,
messages: [
{ role: :incoming, content: 'My order #12345 hasn\'t arrived yet. It was supposed to be here yesterday.' },
{ role: :outgoing, content: 'I apologize for the delay. Let me track that order for you right away.' }
]
},
{
labels: [bulk_order_label.title],
team: sales_team,
priority: nil,
status: :resolved,
messages: [
{ role: :incoming, content: 'We\'re opening 5 new offices and need to set up a recurring order. Can you help?' },
{ role: :outgoing,
content: 'Excellent! We have a bulk program perfect for multi-location businesses. With recurring orders over 250 cases, you get 20% off plus a dedicated account manager. When would you like to discuss the details?' },
{ role: :incoming, content: 'That sounds great! Tomorrow at 2 PM works for me.' },
{ role: :outgoing, content: 'Perfect, I\'ve scheduled a call for tomorrow at 2 PM. I\'ll send you a calendar invite shortly.' }
]
},
{
labels: [product_label.title, pricing_label.title],
team: sales_team,
priority: nil,
status: :open,
messages: [
{ role: :incoming, content: 'Do you have recycled paper options? We\'re trying to go green.' },
{ role: :outgoing,
content: 'Yes! Our Eco-Friendly line is 30% post-consumer recycled content and FSC certified. It\'s $48 per case, just $3 more than standard paper.' }
]
},
{
labels: [custom_print_label.title, urgent_label.title],
team: support_team,
priority: :urgent,
status: :open,
messages: [
{ role: :incoming, content: 'We have a trade show next week and need 1000 business cards ASAP. Can you rush this?' },
{ role: :outgoing,
content: 'For rush orders, we can turn it around in 2-3 business days for an additional $50 rush fee. Would that work for your timeline?' }
]
},
{
labels: [delivery_label.title],
team: operations_team,
priority: nil,
status: :resolved,
messages: [
{ role: :incoming, content: 'What are your delivery hours? We have limited receiving times.' },
{ role: :outgoing,
content: 'Our standard delivery window is 9 AM - 5 PM. We can accommodate specific time windows with advance notice. What times work best for you?' },
{ role: :incoming, content: '10 AM - 2 PM would be ideal.' },
{ role: :outgoing, content: 'Noted! I\'ve added that to your account preferences.' }
]
}
]
puts ' Creating 50 diverse conversations...'
50.times do |i|
# Create contact
customer_name = customer_names[i % customer_names.length]
company = companies[i % companies.length]
contact_inbox = ContactInboxWithContactBuilder.new(
source_id: agents.sample.id,
inbox: [inbox, sales_inbox, support_inbox].sample,
hmac_verified: true,
contact_attributes: {
name: customer_name,
email: "#{customer_name.downcase.tr(' ', '.')}@#{company.downcase.delete(' ')}.com",
phone_number: "+1555#{rand(1_000_000..9_999_999)}",
additional_attributes: {
company_name: company,
city: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'Philadelphia'].sample
}
}
).perform
# Pick a template or create variation
template = conversation_templates[i % conversation_templates.length]
# Assign to agent from appropriate team
team_members = TeamMember.where(team: template[:team]).pluck(:user_id)
assigned_agent = User.find(team_members.sample)
# Create conversation
conv = Conversation.create!(
account: account,
inbox: contact_inbox.inbox,
status: template[:status],
assignee: assigned_agent,
contact: contact_inbox.contact,
contact_inbox: contact_inbox,
priority: template[:priority],
additional_attributes: {
initiated_at: {
timestamp: rand(30).days.ago.to_i
}
}
)
# Add labels
conv.label_list.add(*template[:labels])
conv.save!
# Create messages
template[:messages].each do |msg_template|
sender = msg_template[:role] == :incoming ? contact_inbox.contact : assigned_agent
Message.create!(
content: msg_template[:content],
account: account,
inbox: contact_inbox.inbox,
conversation: conv,
sender: sender,
message_type: msg_template[:role]
)
end
end
# Create one special conversation with interactive messages
special_contact = ContactInboxWithContactBuilder.new(
source_id: sales_lead.id,
contact_inbox = ContactInboxWithContactBuilder.new(
source_id: user.id,
inbox: inbox,
hmac_verified: true,
contact_attributes: {
name: 'Demo Customer',
email: 'demo@example.com',
phone_number: '+15551234567'
}
contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' }
).perform
demo_conversation = Conversation.create!(
conversation = Conversation.create!(
account: account,
inbox: inbox,
status: :open,
assignee: sales_lead,
contact: special_contact.contact,
contact_inbox: special_contact,
assignee: user,
contact: contact_inbox.contact,
contact_inbox: contact_inbox,
additional_attributes: {}
)
demo_conversation.label_list.add(bulk_order_label.title)
demo_conversation.save!
Message.create!(content: 'Hi! I\'m interested in learning more about your products.', account: account, inbox: inbox,
conversation: demo_conversation, sender: special_contact.contact, message_type: :incoming)
# sample email collect
Seeders::MessageSeeder.create_sample_email_collect_message conversation
# Add interactive message samples
Seeders::MessageSeeder.create_sample_cards_message demo_conversation
Seeders::MessageSeeder.create_sample_input_select_message demo_conversation
Seeders::MessageSeeder.create_sample_form_message demo_conversation
Seeders::MessageSeeder.create_sample_articles_message demo_conversation
Seeders::MessageSeeder.create_sample_email_collect_message demo_conversation
Seeders::MessageSeeder.create_sample_csat_collect_message demo_conversation
Message.create!(content: 'Hello', account: account, inbox: inbox, conversation: conversation, sender: contact_inbox.contact,
message_type: :incoming)
puts ' ✓ Created 50+ conversations with diverse scenarios'
# ===============================================
# Help Center Portal Setup
# ===============================================
portal = Portal.create!(
account: account,
name: 'Paperlayer Help Center',
slug: 'dunder-mifflin-help',
page_title: 'Paperlayer Paper Company - Help & Support',
header_text: 'How can we help you with your paper needs?',
homepage_link: 'https://paperlayer.com',
color: '#0066CC',
config: {
'allowed_locales' => ['en'],
'default_locale' => 'en'
}
# sample location message
#
location_message = Message.new(content: 'location', account: account, inbox: inbox, sender: contact_inbox.contact, conversation: conversation,
message_type: :incoming)
location_message.attachments.new(
account_id: account.id,
file_type: 'location',
coordinates_lat: 37.7893768,
coordinates_long: -122.3895553,
fallback_title: 'Bay Bridge, San Francisco, CA, USA'
)
location_message.save!
# Category: Getting Started
getting_started_category = Category.create!(
account: account,
portal: portal,
name: 'Getting Started',
slug: 'getting-started',
description: 'Learn the basics of ordering from Paperlayer',
locale: 'en',
position: 1,
icon: 'book-open'
)
# sample card
Seeders::MessageSeeder.create_sample_cards_message conversation
# input select
Seeders::MessageSeeder.create_sample_input_select_message conversation
# form
Seeders::MessageSeeder.create_sample_form_message conversation
# articles
Seeders::MessageSeeder.create_sample_articles_message conversation
# csat
Seeders::MessageSeeder.create_sample_csat_collect_message conversation
Article.create!(
account: account,
portal: portal,
category: getting_started_category,
author: user,
title: 'How to Place Your First Order',
description: 'Step-by-step guide to placing your first paper order with Paperlayer',
content: "# How to Place Your First Order\n\nWelcome to Paperlayer! We're excited to help you with your paper needs.\n\n## Step 1: Browse Our Products\nVisit our product catalog to explore our wide range of paper products:\n- Copy Paper (20lb, 24lb, 28lb, 32lb)\n- Cardstock (65lb, 80lb, 110lb)\n- Specialty Papers (colored, glossy, recycled)\n\n## Step 2: Request a Quote\nContact our sales team via:\n- Live chat on our website\n- Email: sales@paperlayer.com\n- Phone: 1-800-PAPER-01\n\n## Step 3: Review and Approve\nOur team will send you a detailed quote within 24 hours.\n\n## Step 4: Place Your Order\nOnce approved, we'll process your order and provide tracking information.\n\n## Step 5: Delivery\nStandard delivery: 2-3 business days\nSame-day delivery: Available for local orders placed before noon",
status: :published,
locale: 'en'
)
Article.create!(
account: account,
portal: portal,
category: getting_started_category,
author: user,
title: 'Understanding Paper Weights',
description: 'Learn about different paper weights and their best uses',
content: "# Understanding Paper Weights\n\nPaper weight can be confusing. Let us help you choose the right weight for your needs.\n\n## Copy Paper Weights\n\n### 20lb (75 gsm) - Standard\n- Most common weight for everyday printing\n- Great for internal documents, drafts\n- Economical choice for high-volume printing\n\n### 24lb (90 gsm) - Premium\n- Heavier feel, more professional\n- Ideal for presentations, resumes\n- Less see-through than 20lb\n\n### 28lb (105 gsm) - Super Premium\n- Excellent for important documents\n- Professional brochures\n- High-quality letterhead\n\n### 32lb (120 gsm) - Ultra Premium\n- Luxury feel and appearance\n- Executive communications\n- Certificates and awards\n\n## Cardstock Weights\n\n### 65lb (176 gsm) - Light Cardstock\n- Greeting cards, postcards\n- Thin presentations\n\n### 80lb (216 gsm) - Medium Cardstock\n- Business cards\n- Invitations\n\n### 110lb (298 gsm) - Heavy Cardstock\n- Premium business cards\n- High-end invitations\n- Display materials\n\n## Need Help?\nContact our team at sales@paperlayer.com or call 1-800-PAPER-01",
status: :published,
locale: 'en'
)
# Category: Products & Pricing
products_category = Category.create!(
account: account,
portal: portal,
name: 'Products & Pricing',
slug: 'products-pricing',
description: 'Information about our paper products and pricing',
locale: 'en',
position: 2,
icon: 'tag'
)
Article.create!(
account: account,
portal: portal,
category: products_category,
author: user,
title: 'Copy Paper Product Line',
description: 'Complete guide to our copy paper offerings',
content: "# Copy Paper Product Line\n\nPaperlayer offers the finest copy paper in the Northeast.\n\n## Standard Copy Paper\n**Premium White - 20lb**\n- Price: $45 per case (10 reams)\n- Brightness: 92\n- Size: 8.5\" x 11\"\n- Sheets per ream: 500\n\n**Premium White - 24lb**\n- Price: $52 per case (10 reams)\n- Brightness: 94\n- Size: 8.5\" x 11\"\n- Sheets per ream: 500\n\n## Premium Copy Paper\n**Ultra White - 28lb**\n- Price: $65 per case (10 reams)\n- Brightness: 96\n- Perfect for presentations\n\n**Executive White - 32lb**\n- Price: $78 per case (10 reams)\n- Brightness: 98\n- Luxury feel and appearance\n\n## Recycled Paper\n**Eco-Friendly - 20lb**\n- Price: $48 per case (10 reams)\n- 30% post-consumer content\n- FSC certified\n\n## Volume Discounts\n- 50-99 cases: 10% off\n- 100-249 cases: 15% off\n- 250+ cases: 20% off\n\nContact us for custom quotes!",
status: :published,
locale: 'en'
)
Article.create!(
account: account,
portal: portal,
category: products_category,
author: user,
title: 'Bulk Order Discounts',
description: 'Learn about our volume pricing tiers and save on large orders',
content: "# Bulk Order Discounts\n\nThe more you order, the more you save with Paperlayer!\n\n## Discount Tiers\n\n### Tier 1: 50-99 Cases\n- **10% discount**\n- Minimum order: 50 cases\n- Perfect for medium-sized offices\n\n### Tier 2: 100-249 Cases\n- **15% discount**\n- Quarterly supply option\n- Dedicated account manager\n\n### Tier 3: 250+ Cases\n- **20% discount**\n- Annual contract pricing\n- Priority customer service\n- Free delivery\n- Flexible payment terms\n\n## Example Savings\n\n**Standard scenario: 100 cases of Premium White 20lb**\n- Regular price: $4,500\n- With 15% discount: $3,825\n- **You save: $675!**\n\n## Additional Benefits\n- No order minimums after initial bulk purchase\n- Locked-in pricing for contract duration\n- Scheduled deliveries\n- Custom invoicing options\n\n## Get Started\nContact our bulk sales team:\n- Email: bulk@paperlayer.com\n- Phone: 1-800-BULK-PAPER\n- Live chat: Available 9 AM - 6 PM EST",
status: :published,
locale: 'en'
)
# Category: Shipping & Delivery
shipping_category = Category.create!(
account: account,
portal: portal,
name: 'Shipping & Delivery',
slug: 'shipping-delivery',
description: 'Delivery options, shipping costs, and tracking information',
locale: 'en',
position: 3,
icon: 'truck'
)
Article.create!(
account: account,
portal: portal,
category: shipping_category,
author: user,
title: 'Delivery Options & Times',
description: 'Choose the delivery option that works best for you',
content: "# Delivery Options & Times\n\n## Standard Delivery (FREE)\n- **Timeframe:** 2-3 business days\n- **Available:** All orders over $100\n- **Coverage:** Within 200 miles of Scranton, PA\n\n## Express Delivery\n- **Timeframe:** Next business day\n- **Cost:** $25 flat fee\n- **Coverage:** Within 100 miles\n- **Cutoff:** Order by 3 PM\n\n## Same-Day Delivery\n- **Timeframe:** Within 6 hours\n- **Cost:** $50 flat fee\n- **Coverage:** Within 50 miles\n- **Cutoff:** Order by 12 PM noon\n\n## Freight Shipping (Bulk Orders)\n- **Timeframe:** 3-5 business days\n- **Cost:** Varies by distance and volume\n- **Available:** Orders over 100 cases\n- **Includes:** Forklift delivery available\n\n## Tracking Your Order\nAll orders include:\n- Real-time tracking number\n- Email notifications\n- Estimated delivery window\n- Delivery signature confirmation\n\n## Delivery Notes\n- Residential deliveries available\n- Loading dock delivery preferred for bulk\n- Inside delivery available (additional fee)\n- Weather delays may occur\n\nQuestions? Contact: logistics@paperlayer.com",
status: :published,
locale: 'en'
)
# Category: Custom Services
custom_category = Category.create!(
account: account,
portal: portal,
name: 'Custom Services',
slug: 'custom-services',
description: 'Custom printing, letterhead, and specialty services',
locale: 'en',
position: 4,
icon: 'printer'
)
Article.create!(
account: account,
portal: portal,
category: custom_category,
author: user,
title: 'Custom Letterhead & Business Stationery',
description: 'Professional custom printing services for your business',
content: "# Custom Letterhead & Business Stationery\n\nMake a lasting impression with custom-printed materials from Paperlayer.\n\n## Letterhead Services\n\n### Standard Letterhead\n- **Minimum order:** 5 reams (2,500 sheets)\n- **Turnaround:** 5-7 business days\n- **Paper weight:** 24lb or 28lb\n- **Colors:** Up to 4-color process\n- **Starting at:** $15 per ream\n\n### Premium Letterhead\n- **Minimum order:** 5 reams\n- **Turnaround:** 7-10 business days\n- **Paper weight:** 32lb premium stock\n- **Options:** Embossing, foil stamping\n- **Starting at:** $25 per ream\n\n## Business Cards\n- Standard: 14pt cardstock\n- Premium: 16pt with UV coating\n- Minimum: 500 cards\n- Turnaround: 3-5 business days\n\n## Envelopes\n- Custom printed envelopes\n- Standard #10 size\n- Match your letterhead design\n- Minimum: 500 envelopes\n\n## Design Services\n- Professional design assistance: $150\n- Logo digitization: $75\n- Proof revisions: Included (up to 3)\n\n## The Process\n\n1. **Submit your design** - Email to custom@paperlayer.com\n2. **Review proof** - We'll send a digital proof within 2 business days\n3. **Approve** - Make any needed revisions\n4. **Production** - We print your order\n5. **Delivery** - Ships via your chosen method\n\n## File Requirements\n- Format: PDF, AI, or EPS\n- Resolution: 300 DPI minimum\n- Color mode: CMYK\n- Bleed: 0.125\" on all sides\n\nNeed help with design? We can assist!\nEmail: custom@paperlayer.com\nPhone: 1-800-CUSTOM-PAPER",
status: :published,
locale: 'en'
)
# ===============================================
# Captain Assistant Setup (Enterprise Feature)
# ===============================================
if defined?(Captain::Assistant)
captain_assistant = Captain::Assistant.create!(
account: account,
name: 'Paper Expert',
description: 'AI assistant specialized in helping customers with all their paper product needs, from product selection to bulk orders.',
config: {
temperature: 0.7,
feature_faq: true,
feature_memory: true,
product_name: 'Paperlayer Paper Products'
},
response_guidelines: [
'Always be friendly and professional',
'Use paper industry terminology when appropriate',
'Provide specific product recommendations based on customer needs',
'Mention bulk discounts when discussing large orders',
'Offer to connect customers with sales team for custom quotes'
],
guardrails: [
'Do not discuss competitor products',
'Do not make promises about delivery times without confirming inventory',
'Always verify bulk pricing with sales team for orders over 250 cases'
]
)
# Connect assistant to inbox
CaptainInbox.create!(
captain_assistant: captain_assistant,
inbox: inbox
)
# Scenario 1: Product Recommendations
Captain::Scenario.create!(
account: account,
assistant: captain_assistant,
title: 'Product Recommendations',
description: 'Help customers choose the right paper products for their needs',
instruction: "When a customer asks about which paper to use, ask clarifying questions:\n1. What will they use it for? (everyday printing, presentations, letterhead, etc.)\n2. What's their volume? (help them understand bulk discounts)\n3. Do they have any special requirements? (recycled, brightness, specific sizes)\n\nThen recommend appropriate products from our catalog:\n- 20lb for everyday use\n- 24lb for professional documents\n- 28lb for presentations\n- 32lb for executive communications\n- Cardstock for business cards\n\nAlways mention relevant discounts and offer to connect them with sales for quotes.",
enabled: true
)
# Scenario 2: Bulk Orders
Captain::Scenario.create!(
account: account,
assistant: captain_assistant,
title: 'Bulk Order Processing',
description: 'Guide customers through bulk order process and pricing',
instruction: "For bulk order inquiries:\n1. Determine their quantity needs\n2. Explain applicable discount tiers:\n - 50-99 cases: 10% off\n - 100-249 cases: 15% off\n - 250+ cases: 20% off\n3. Calculate estimated savings\n4. Explain additional benefits (account manager, flexible terms)\n5. Offer to connect with bulk sales team for formal quote\n6. Add the 'bulk-order' label to the conversation",
enabled: true
)
# Scenario 3: Shipping Questions
Captain::Scenario.create!(
account: account,
assistant: captain_assistant,
title: 'Shipping & Delivery',
description: 'Answer questions about delivery options and times',
instruction: "When customers ask about shipping:\n1. Standard delivery: 2-3 business days (FREE over $100)\n2. Express: Next day ($25)\n3. Same-day: Within 6 hours, order by noon ($50)\n4. Bulk freight: 3-5 days for 100+ cases\n\nMention tracking is included with all orders.\nFor urgent needs, emphasize same-day delivery within 50 miles.",
enabled: true
)
# Document 1: FAQ - General Questions
Captain::Document.create!(
account: account,
assistant: captain_assistant,
name: 'General FAQs',
external_link: 'https://paperlayer.com/faq/general',
content: "# Frequently Asked Questions\n\n## What are your business hours?\nMonday-Friday: 8 AM - 6 PM EST\nSaturday: 9 AM - 2 PM EST\nSunday: Closed\n\n## Do you offer free shipping?\nYes! Free standard shipping on all orders over $100 within 200 miles of Scranton, PA.\n\n## What payment methods do you accept?\n- Credit cards (Visa, Mastercard, AmEx, Discover)\n- Purchase orders (for established accounts)\n- ACH/Wire transfer\n- Net 30 terms (for qualified businesses)\n\n## Can I return paper products?\nYes, unopened reams can be returned within 30 days for a full refund.\n\n## Do you have a showroom?\nYes! Visit our Scranton office at:\n1725 Slough Avenue\nScranton, PA 18505\n\nAppointments recommended.\n\n## What makes Paperlayer different?\n- Family-owned since 1949\n- Personal service from dedicated account managers\n- Local delivery and same-day service\n- Competitive pricing with volume discounts\n- Expert advice on paper selection",
status: :available
)
# Document 2: FAQ - Product Specifications
Captain::Document.create!(
account: account,
assistant: captain_assistant,
name: 'Product Specifications',
external_link: 'https://paperlayer.com/faq/products',
content: "# Product Specifications FAQs\n\n## What does paper brightness mean?\nBrightness is measured on a scale of 0-100. Higher numbers mean whiter, brighter paper.\n- 92: Standard brightness\n- 94-96: Premium brightness\n- 98+: Ultra-premium brightness\n\n## What's the difference between 20lb and 24lb paper?\nThe number refers to the weight of 500 sheets (a ream).\n- 20lb: Standard weight, economical\n- 24lb: Heavier, more professional feel\n- 28lb: Premium weight for presentations\n- 32lb: Luxury weight for executive use\n\n## Is your paper acid-free?\nYes, all our copy paper is acid-free for long-term archival quality.\n\n## Do you carry recycled paper?\nYes! Our Eco-Friendly line contains 30% post-consumer recycled content and is FSC certified.\n\n## What sizes do you offer?\n- Letter: 8.5\" x 11\" (most common)\n- Legal: 8.5\" x 14\"\n- Tabloid: 11\" x 17\"\n- Custom sizes available for bulk orders\n\n## Can I get paper in colors?\nYes! We stock:\n- Pastels: Ivory, Pink, Blue, Green, Yellow\n- Brights: Red, Orange, Purple\n- Custom colors available for bulk orders\n\n## What's the shelf life of paper?\nPaper properly stored in a cool, dry place will last indefinitely.",
status: :available
)
# Document 3: Bulk Ordering Guide
Captain::Document.create!(
account: account,
assistant: captain_assistant,
name: 'Bulk Ordering Guide',
external_link: 'https://paperlayer.com/guides/bulk-orders',
content: "# Bulk Ordering Guide\n\n## Benefits of Bulk Orders\n\n### Cost Savings\n- 50-99 cases: Save 10%\n- 100-249 cases: Save 15%\n- 250+ cases: Save 20%\n\n### Additional Perks\n- Dedicated account manager\n- Priority customer service\n- Flexible payment terms (Net 30/60/90)\n- Free delivery on orders over 100 cases\n- Scheduled recurring deliveries\n- Custom reporting and invoicing\n\n## How to Place a Bulk Order\n\n1. **Contact our bulk sales team**\n - Email: bulk@paperlayer.com\n - Phone: 1-800-BULK-PAPER\n - Chat with us online\n\n2. **Discuss your needs**\n - Types of paper needed\n - Estimated monthly/annual volume\n - Delivery schedule preferences\n\n3. **Receive your custom quote**\n - Within 24 hours for standard requests\n - Same day for urgent needs\n\n4. **Review and sign agreement**\n - No long-term contracts required\n - Flexible terms available\n\n5. **Start saving!**\n - Automated ordering available\n - Just-in-time delivery options\n\n## Storage Tips for Bulk Orders\n- Store in cool, dry place (60-70°F)\n- Keep away from direct sunlight\n- Maintain 40-60% humidity\n- Store flat, not on edge\n- Keep in original packaging until use\n\n## Popular Bulk Order Scenarios\n\n### Small Office (50-100 cases/year)\n- Mix of 20lb and 24lb paper\n- Quarterly deliveries\n- 10% savings tier\n\n### Medium Business (100-250 cases/year)\n- Multiple paper types\n- Monthly deliveries\n- 15% savings tier\n- Account manager included\n\n### Large Corporation (250+ cases/year)\n- Enterprise agreement\n- Custom delivery schedule\n- 20% savings tier\n- Premium support\n\n## Questions?\nContact our bulk sales specialists:\nbulk@paperlayer.com",
status: :available
)
# Document 4: Custom Printing Guide
Captain::Document.create!(
account: account,
assistant: captain_assistant,
name: 'Custom Printing Services',
external_link: 'https://paperlayer.com/services/custom-printing',
content: "# Custom Printing Services\n\n## What We Offer\n\n### Letterhead\n- Professional custom letterhead\n- Minimum: 5 reams (2,500 sheets)\n- Paper options: 24lb, 28lb, or 32lb\n- Full color printing\n- Turnaround: 5-10 business days\n\n### Business Cards\n- Premium cardstock\n- Standard or custom sizes\n- Options: matte, gloss, UV coating\n- Minimum: 500 cards\n- Turnaround: 3-5 business days\n\n### Envelopes\n- Custom printed envelopes\n- Match your letterhead\n- Multiple sizes available\n- Minimum: 500 envelopes\n\n## Design Services\n\nNeed help with design?\n- Professional design: $150\n- Logo digitization: $75\n- Free setup for repeat orders\n- Up to 3 proof revisions included\n\n## File Requirements\n- Preferred format: PDF, AI, EPS\n- Resolution: 300 DPI minimum\n- Color mode: CMYK (not RGB)\n- Include 0.125\" bleed\n- Embed all fonts\n\n## Approval Process\n\n1. Submit your design or request assistance\n2. Receive digital proof within 2 business days\n3. Review and approve (or request changes)\n4. Production begins upon approval\n5. Delivery within specified timeframe\n\n## Pricing Examples\n\n**Standard Letterhead (24lb)**\n- 5 reams: $75\n- 10 reams: $140\n- 25 reams: $325\n\n**Premium Letterhead (32lb)**\n- 5 reams: $125\n- 10 reams: $230\n- 25 reams: $525\n\n**Business Cards**\n- 500 cards: $75\n- 1,000 cards: $125\n- 2,500 cards: $275\n\n## Get Started\n\nEmail your project details to:\ncustom@paperlayer.com\n\nOr call: 1-800-CUSTOM-PAPER",
status: :available
)
puts ' ✓ Captain Assistant created with documents and scenarios'
else
puts ' ⚠ Captain Assistant is an Enterprise feature - skipping'
end
CannedResponse.create!(account: account, short_code: 'start', content: 'Hello welcome to chatwoot.')
end
+5 -189
View File
@@ -23,45 +23,22 @@ class Seeders::AccountSeeder
set_up_users
seed_labels
seed_canned_responses
seed_macros
seed_inboxes
seed_contacts
seed_portals
seed_captain_assistants
finalize_seeding
end
def set_up_account
# Delete all existing data before seeding
@account.macros.destroy_all
@account.canned_responses.destroy_all
@account.teams.destroy_all
@account.conversations.destroy_all
@account.labels.destroy_all
@account.portals.destroy_all
@account.inboxes.destroy_all
@account.contacts.destroy_all
@account.custom_roles.destroy_all if @account.respond_to?(:custom_roles)
# Delete Captain Assistants if Enterprise feature is available
return unless defined?(Captain::Assistant)
Captain::Assistant.where(account_id: @account.id).destroy_all
end
def seed_teams
@account_data['teams'].each do |team_data|
if team_data.is_a?(String)
# Support legacy format (just team name)
@account.teams.create!(name: team_data)
else
# New format with name and description
@account.teams.create!(
name: team_data['name'],
description: team_data['description'],
allow_auto_assign: team_data['allow_auto_assign'] != false
)
end
@account_data['teams'].each do |team_name|
@account.teams.create!(name: team_name)
end
end
@@ -126,68 +103,9 @@ class Seeders::AccountSeeder
@account.custom_roles.find_by(name: role_name)
end
def seed_canned_responses
return unless @account_data['canned_responses'].present?
@account_data['canned_responses'].each do |response|
@account.canned_responses.create!(response)
end
end
def seed_macros
return unless @account_data['macros'].present?
@account_data['macros'].each do |macro_data|
created_by = User.from_email(macro_data['created_by']) if macro_data['created_by'].present?
actions = macro_data['actions'].map do |action|
process_macro_action(action)
end.compact # Remove nil actions (when team/agent not found)
# Only create macro if there are valid actions
if actions.any?
@account.macros.create!(
name: macro_data['name'],
visibility: macro_data['visibility'] || 'global',
created_by: created_by,
actions: actions
)
else
Rails.logger.warn("Macro '#{macro_data['name']}' has no valid actions, skipping")
end
end
end
def seed_portals
return unless @account_data['portals'].present?
@account_data['portals'].each do |portal_data|
portal = @account.portals.create!(
name: portal_data['name'],
slug: portal_data['slug'],
page_title: portal_data['page_title'],
header_text: portal_data['header_text'],
color: portal_data['color'],
config: portal_data['config']
)
seed_portal_categories(portal, portal_data['categories']) if portal_data['categories'].present?
end
end
def seed_captain_assistants
return unless @account_data['captain_assistants'].present?
return unless defined?(Captain::Assistant)
@account_data['captain_assistants'].each do |assistant_data|
assistant = Captain::Assistant.create!(
account: @account,
name: assistant_data['name'],
description: assistant_data['description'],
config: assistant_data['config'] || {}
)
seed_captain_documents(assistant, assistant_data['documents']) if assistant_data['documents'].present?
seed_captain_scenarios(assistant, assistant_data['scenarios']) if assistant_data['scenarios'].present?
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
@@ -237,106 +155,4 @@ class Seeders::AccountSeeder
def seed_inboxes
Seeders::InboxSeeder.new(account: @account, company_data: @account_data[:company]).perform!
end
def process_macro_action(action)
processed_action = { 'action_name' => action['action_name'] }
case action['action_name']
when 'assign_team'
team = @account.teams.find_by('name LIKE ?', "%#{action['action_params'][0]}%")
if team
processed_action['action_params'] = [team.id]
else
Rails.logger.warn("Macro: Team not found matching '#{action['action_params'][0]}', skipping assign_team action")
return nil # Skip this action
end
when 'assign_agent'
user = User.from_email(action['action_params'][0])
if user
processed_action['action_params'] = [user.id]
else
Rails.logger.warn("Macro: User not found '#{action['action_params'][0]}', skipping assign_agent action")
return nil # Skip this action
end
else
processed_action['action_params'] = action['action_params']
end
processed_action
end
def seed_portal_categories(portal, categories_data)
categories_data.each do |category_data|
category = portal.categories.create!(
account: @account,
name: category_data['name'],
slug: category_data['slug'],
description: category_data['description'],
locale: category_data['locale'] || 'en',
position: category_data['position']
)
seed_category_articles(portal, category, category_data['articles']) if category_data['articles'].present?
end
end
def seed_category_articles(portal, category, articles_data)
articles_data.each do |article_data|
author = User.from_email(article_data['author']) if article_data['author'].present?
author ||= @account.users.administrators.first
portal.articles.create!(
account: @account,
category: category,
author: author,
title: article_data['title'],
description: article_data['description'],
content: article_data['content'],
status: article_data['status'] || 'published',
locale: article_data['locale'] || 'en'
)
end
end
def seed_captain_documents(assistant, documents_data)
documents_data.each do |doc_data|
Captain::Document.create!(
account: @account,
assistant: assistant,
name: doc_data['name'],
external_link: doc_data['external_link'],
content: doc_data['content'],
status: doc_data['status'] || 'available'
)
end
end
def seed_captain_scenarios(assistant, scenarios_data)
scenarios_data.each do |scenario_data|
Captain::Scenario.create!(
account: @account,
assistant: assistant,
title: scenario_data['title'],
description: scenario_data['description'],
instruction: scenario_data['instruction'],
enabled: scenario_data['enabled'] != false
)
end
end
def finalize_seeding
# Reload account to ensure all associations are fresh
@account.reload
# Reset cache keys for labels, inboxes, teams
@account.reset_cache_keys if @account.respond_to?(:reset_cache_keys)
# Clear any Rails cache related to this account
Rails.cache.delete("account/#{@account.id}")
# Force reload of all account users to refresh their associations
@account.account_users.each(&:reload)
Rails.logger.info("Seed completed for account #{@account.id} - #{@account.name}")
end
end
+24 -32
View File
@@ -6,7 +6,7 @@ module Seeders::MessageSeeder
conversation: conversation,
message_type: :template,
content_type: :input_email,
content: 'Want us to email you a copy of your quote?'
content: 'Get notified by email'
)
end
@@ -17,7 +17,7 @@ module Seeders::MessageSeeder
conversation: conversation,
message_type: :template,
content_type: :input_csat,
content: 'How would you rate your experience with Paperlayer today?'
content: 'Please rate the support'
)
end
@@ -41,18 +41,18 @@ module Seeders::MessageSeeder
def self.sample_card_item
{
media_url: 'https://i.imgur.com/d8Djr4k.jpg',
title: 'Premium Copy Paper - 20lb',
description: 'Bright white, 500 sheets per ream. Perfect for everyday printing.',
title: 'Acme Shoes 2.0',
description: 'Move with Acme Shoe 2.0',
actions: [
{
type: 'link',
text: 'View Details',
uri: 'http://paperlayer.com/products/copy-paper'
text: 'View More',
uri: 'http://acme-shoes.inc'
},
{
type: 'postback',
text: 'Request Quote',
payload: 'QUOTE_REQUESTED'
text: 'Add to cart',
payload: 'ITEM_SELECTED'
}
]
}
@@ -64,14 +64,14 @@ module Seeders::MessageSeeder
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content: 'What type of paper are you interested in?',
content: 'Your favorite food',
content_type: 'input_select',
content_attributes: {
items: [
{ title: '📄 Copy Paper (20lb)', value: 'copy-paper-20' },
{ title: '📋 Cardstock (110lb)', value: 'cardstock-110' },
{ title: '🎨 Colored Paper', value: 'colored-paper' },
{ title: '✨ Premium (32lb)', value: 'premium-32' }
{ title: '🌯 Burito', value: 'Burito' },
{ title: '🍝 Pasta', value: 'Pasta' },
{ title: ' 🍱 Sushi', value: 'Sushi' },
{ title: ' 🥗 Salad', value: 'Salad' }
]
}
)
@@ -92,20 +92,14 @@ module Seeders::MessageSeeder
def self.sample_form
{
items: [
{ name: 'company', placeholder: 'Your company name', type: 'text', label: 'Company Name', required: 'required',
pattern_error: 'Please enter your company name' },
{ name: 'email', placeholder: 'your.email@company.com', type: 'email', label: 'Email Address', required: 'required',
pattern_error: 'Please enter a valid email', pattern: '^[^\s@]+@[^\s@]+\.[^\s@]+$' },
{ name: 'quantity', placeholder: 'e.g., 50 cases', type: 'text', label: 'Quantity Needed', required: 'required',
pattern_error: 'Please specify quantity' },
{ name: 'paper_type', label: 'Paper Type', type: 'select', options: [
{ label: '📄 Copy Paper (20lb)', value: 'copy-20' },
{ label: '📋 Cardstock (110lb)', value: 'cardstock-110' },
{ label: '🎨 Colored Paper', value: 'colored' },
{ label: '✨ Premium (32lb)', value: 'premium-32' }
] },
{ name: 'requirements', placeholder: 'Any special requirements or delivery instructions', type: 'text_area',
label: 'Additional Notes', pattern_error: 'Please fill this field' }
{ 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
@@ -116,14 +110,12 @@ module Seeders::MessageSeeder
inbox: conversation.inbox,
conversation: conversation,
message_type: :template,
content: 'Helpful Resources',
content: 'Tech Companies',
content_type: 'article',
content_attributes: {
items: [
{ title: 'Paper Weight Guide', description: 'Understanding paper weights and their best uses',
link: 'http://paperlayer.com/help/paper-weights' },
{ title: 'Bulk Order Discounts', description: 'Learn about our volume pricing tiers', link: 'http://paperlayer.com/help/bulk-pricing' },
{ title: 'Custom Printing Options', description: 'Letterhead, envelopes, and branded materials', link: 'http://paperlayer.com/help/custom-printing' }
{ title: 'Acme Hardware', description: 'Hardware reimagined', link: 'http://acme-hardware.inc' },
{ title: 'Acme Search', description: 'The best Search Engine', link: 'http://acme-search.inc' }
]
}
)
+392 -1231
View File
File diff suppressed because it is too large Load Diff
@@ -46,8 +46,7 @@ RSpec.describe AccountDeletionService do
# Reload the user to get the updated email
user_with_one_account.reload
expect(user_with_one_account.email).to eq("#{user_with_one_account.id}@chatwoot-deleted.invalid")
expect(user_with_one_account.email).not_to eq(original_email)
expect(user_with_one_account.email).to eq("#{original_email}-deleted.com")
end
it 'does not modify emails for users belonging to multiple accounts' do
@@ -102,65 +102,6 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
end
context 'when message is a reply (has context)' do
let(:reply_params) do
{
phone_number: whatsapp_channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Pranav' }, wa_id: '16503071063' }],
messages: [{
context: {
from: '16503071063',
id: 'wamid.ORIGINAL_MESSAGE_ID'
},
from: '16503071063',
id: 'wamid.REPLY_MESSAGE_ID',
timestamp: '1770407829',
text: { body: 'This is a reply' },
type: 'text'
}]
}
}]
}]
}.with_indifferent_access
end
context 'when the original message exists in Chatwoot' do
it 'sets in_reply_to to reference the existing message' do
# Create a conversation and the original message that will be replied to first
contact = create(:contact, phone_number: '+16503071063', account: whatsapp_channel.account)
contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_channel.inbox, source_id: '16503071063')
conversation = create(:conversation, contact: contact, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
original_message = create(:message,
conversation: conversation,
source_id: 'wamid.ORIGINAL_MESSAGE_ID',
content: 'Original message')
described_class.new(inbox: whatsapp_channel.inbox, params: reply_params).perform
reply_message = whatsapp_channel.inbox.messages.last
expect(reply_message.content).to eq('This is a reply')
expect(reply_message.content_attributes['in_reply_to']).to eq(original_message.id)
expect(reply_message.content_attributes['in_reply_to_external_id']).to eq('wamid.ORIGINAL_MESSAGE_ID')
end
end
context 'when the original message does not exist in Chatwoot' do
it 'does not set in_reply_to (discards the reply reference)' do
described_class.new(inbox: whatsapp_channel.inbox, params: reply_params).perform
reply_message = whatsapp_channel.inbox.messages.last
expect(reply_message.content).to eq('This is a reply')
expect(reply_message.content_attributes['in_reply_to']).to be_nil
expect(reply_message.content_attributes['in_reply_to_external_id']).to be_nil
end
end
end
end
# Métodos auxiliares para reduzir o tamanho do exemplo