Merge branch 'develop' into fix/editor-heading-error

This commit is contained in:
Nithin David Thomas
2024-01-25 20:02:16 -08:00
committed by GitHub
24 changed files with 221 additions and 105 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ gem 'devise_token_auth'
gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate', '>= 0.19.0'
gem 'administrate', '>= 0.20.1'
gem 'administrate-field-active_storage', '>= 1.0.1'
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
+8 -8
View File
@@ -105,12 +105,12 @@ GEM
activerecord (>= 6.0, < 7.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
administrate (0.19.0)
actionpack (>= 5.0)
actionview (>= 5.0)
activerecord (>= 5.0)
jquery-rails (>= 4.0)
kaminari (>= 1.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
activerecord (>= 6.0, < 8.0)
jquery-rails (~> 4.6.0)
kaminari (~> 1.2.2)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
administrate-field-active_storage (1.0.1)
@@ -461,7 +461,7 @@ GEM
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0)
minitest (5.21.2)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -840,7 +840,7 @@ DEPENDENCIES
active_record_query_trace
activerecord-import
acts-as-taggable-on
administrate (>= 0.19.0)
administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.1)
administrate-field-belongs_to_search (>= 0.9.0)
annotate
@@ -1,7 +1,7 @@
<template>
<div class="w-full">
<div
class="my-0 py-0 px-4 grid grid-cols-6 md:grid-cols-7 lg:grid-cols-8 gap-4 border-b border-slate-100 dark:border-slate-700 sticky top-16 bg-white dark:bg-slate-900"
class="my-0 py-0 px-4 grid grid-cols-6 z-10 md:grid-cols-7 lg:grid-cols-8 gap-4 border-b border-slate-100 dark:border-slate-700 sticky top-16 bg-white dark:bg-slate-900"
:class="{ draggable: onCategoryPage }"
>
<div
+2 -1
View File
@@ -6,7 +6,8 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
def perform(channel)
return unless should_fetch_email?(channel)
with_lock(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id) do
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
with_lock(key, 5.minutes) do
process_email_for_channel(channel)
end
rescue *ExceptionList::IMAP_EXCEPTIONS => e
+24 -8
View File
@@ -14,20 +14,36 @@
class MutexApplicationJob < ApplicationJob
class LockAcquisitionError < StandardError; end
def with_lock(key_format, *args)
lock_key = format(key_format, *args)
def with_lock(lock_key, timeout = Redis::LockManager::LOCK_TIMEOUT)
lock_manager = Redis::LockManager.new
begin
if lock_manager.lock(lock_key)
Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
if lock_manager.lock(lock_key, timeout)
log_attempt(lock_key, executions)
yield
# release the lock after the block has been executed
lock_manager.unlock(lock_key)
else
Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
handle_failed_lock_acquisition(lock_key)
end
ensure
lock_manager.unlock(lock_key)
rescue StandardError => e
handle_error(e, lock_manager, lock_key)
end
end
private
def log_attempt(lock_key, executions)
Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
end
def handle_error(err, lock_manager, lock_key)
lock_manager.unlock(lock_key) unless err.is_a?(LockAcquisitionError)
raise err
end
def handle_failed_lock_acquisition(lock_key)
Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
end
end
+2 -1
View File
@@ -3,7 +3,8 @@ class SendOnSlackJob < MutexApplicationJob
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
def perform(message, hook)
with_lock(::Redis::Alfred::SLACK_MESSAGE_MUTEX, conversation_id: message.conversation_id, reference_id: hook.reference_id) do
key = format(::Redis::Alfred::SLACK_MESSAGE_MUTEX, conversation_id: message.conversation_id, reference_id: hook.reference_id)
with_lock(key) do
Integrations::Slack::SendOnSlackService.new(message: message, hook: hook).perform
end
end
+2 -1
View File
@@ -5,7 +5,8 @@ class Webhooks::FacebookEventsJob < MutexApplicationJob
def perform(message)
response = ::Integrations::Facebook::MessageParser.new(message)
with_lock(::Redis::Alfred::FACEBOOK_MESSAGE_MUTEX, sender_id: response.sender_id, recipient_id: response.recipient_id) do
key = format(::Redis::Alfred::FACEBOOK_MESSAGE_MUTEX, sender_id: response.sender_id, recipient_id: response.recipient_id)
with_lock(key) do
process_message(response)
end
end
+2 -1
View File
@@ -12,7 +12,8 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
def perform(entries)
@entries = entries
with_lock(::Redis::Alfred::IG_MESSAGE_MUTEX, sender_id: sender_id, ig_account_id: ig_account_id) do
key = format(::Redis::Alfred::IG_MESSAGE_MUTEX, sender_id: sender_id, ig_account_id: ig_account_id)
with_lock(key) do
process_entries(entries)
end
end
+5 -8
View File
@@ -6,11 +6,12 @@ class ReplyMailbox < ApplicationMailbox
EMAIL_PART_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i
before_processing :conversation_uuid_from_to_address,
:find_relative_conversation,
:verify_decoded_params,
:decorate_mail
:find_relative_conversation
def process
return if @conversation.blank?
decorate_mail
create_message
add_attachments_to_message
end
@@ -78,12 +79,8 @@ class ReplyMailbox < ApplicationMailbox
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
end
def verify_decoded_params
raise 'Conversation uuid not found' if conversation_uuid.nil?
end
def validate_resource(resource)
raise "Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
Rails.logger.error "[App::Mailboxes::ReplyMailbox] Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
resource
end
+5
View File
@@ -6,10 +6,13 @@
#
# id :integer not null, primary key
# additional_attributes :jsonb
# contact_type :integer default("visitor")
# custom_attributes :jsonb
# email :string
# identifier :string
# last_activity_at :datetime
# last_name :string default("")
# middle_name :string default("")
# name :string default("")
# phone_number :string
# created_at :datetime not null
@@ -55,6 +58,8 @@ class Contact < ApplicationRecord
after_update_commit :dispatch_update_event
after_destroy_commit :dispatch_destroy_event
enum contact_type: { visitor: 0, lead: 1, customer: 2 }
scope :order_on_last_activity_at, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
+1
View File
@@ -60,6 +60,7 @@ class Notification < ApplicationRecord
secondary_actor: secondary_actor&.push_event_data,
user: user&.push_event_data,
created_at: created_at.to_i,
last_activity_at: last_activity_at.to_i,
account_id: account_id
}
@@ -19,7 +19,13 @@ class Whatsapp::IncomingMessageBaseService
private
def process_messages
# message allready exists so we don't need to process
# We don't support reactions & ephemeral message now, we need to skip processing the message
# if the webhook event is a reaction or an ephermal message or an unsupported message.
return if unprocessable_message_type?(message_type)
# Multiple webhook event can be received against the same message due to misconfigurations in the Meta
# business manager account. While we have not found the core reason yet, the following line ensure that
# there are no duplicate messages created.
return if find_message_by_source_id(@processed_params[:messages].first[:id]) || message_under_process?
cache_message_source_id_in_redis
@@ -49,8 +55,6 @@ class Whatsapp::IncomingMessageBaseService
end
def create_messages
return if unprocessable_message_type?(message_type)
message = @processed_params[:messages].first
log_error(message) && return if error_webhook_event?(message)
@@ -44,7 +44,7 @@ module Whatsapp::IncomingMessageServiceHelpers
end
def unprocessable_message_type?(message_type)
%w[reaction ephemeral unsupported].include?(message_type)
%w[reaction ephemeral unsupported request_welcome].include?(message_type)
end
def brazil_phone_number?(phone_number)
@@ -30,9 +30,8 @@
</h1>
<div class="flex flex-col items-start justify-between w-full pt-6 md:flex-row md:items-center">
<div class="flex items-start space-x-1">
<div class="pr-px mt-0.5 min-w-[20px] min-h-[20px]">
<%= render "public/api/v1/portals/thumbnail", author: article.author, size: 5 %>
</div>
<span class="flex items-center text-base font-medium text-slate-600 dark:text-slate-400"><%= "#{I18n.t('public_portal.common.by')} #{article.author&.available_name || article.author&.name || ''}" %><%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %></span>
<span class="flex items-center text-base font-medium text-slate-600 dark:text-slate-400">
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %>
</span>
</div>
</div>
@@ -32,17 +32,7 @@
<h3 id="<%= !@is_plain_layout_enabled ? 'category-name' : '' %>" class="text-lg text-slate-900 tracking-[0.28px] dark:text-slate-50 font-semibold <%= @is_plain_layout_enabled ? 'group-hover:underline' : '' %>"><%= article.title %></h3>
<p class="text-base font-normal text-slate-600 dark:text-slate-200 line-clamp-1 break-all"><%= render_category_content(article.content) %></p>
</div>
<div class="flex flex-row items-center gap-1">
<div class="flex flex-row items-center gap-1">
<% author = article.author %>
<%= render "public/api/v1/portals/thumbnail", author: author, size: 5 %>
<% if author&.name.present? %>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium flex items-center"><%= "#{I18n.t('public_portal.common.by')} #{author&.name}" %></span>
<% end %>
</div>
<span class="text-slate-600 dark:text-slate-400"></span>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium flex items-center"><%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %></span>
</div>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium flex items-center"><%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %></span>
</div>
</a>
</div>
+26 -16
View File
@@ -34,16 +34,24 @@ as well as a link to its edit page.
<section class="main-content__body">
<dl>
<% page.attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: attribute.name.titleize,
) %>
</dt>
<% page.attributes.each do |title, attributes| %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</dt>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% end %>
</fieldset>
<% end %>
</dl>
</section>
@@ -69,13 +77,15 @@ as well as a link to its edit page.
</div>
<% end %>
<% account_user_page.attributes.each do |attribute| -%>
<% if attribute.name == "account" %>
<%= f.hidden_field('account_id', value: page.resource.id) %>
<% else %>
<div class="field-unit field-unit--<%= attribute.html_class %> field-unit--<%= requireness(attribute) %>">
<%= render_field attribute, f: f %>
</div>
<% account_user_page.attributes.each do |title, attributes| -%>
<% attributes.each do |attribute| %>
<% if attribute.name == "account" %>
<%= f.hidden_field('account_id', value: page.resource.id) %>
<% else %>
<div class="field-unit field-unit--<%= attribute.html_class %> field-unit--<%= requireness(attribute) %>">
<%= render_field attribute, f: f %>
</div>
<% end %>
<% end %>
<% end -%>
+27 -17
View File
@@ -34,16 +34,24 @@ as well as a link to its edit page.
<section class="main-content__body">
<dl>
<% page.attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: attribute.name.titleize,
) %>
</dt>
<% page.attributes.each do |title, attributes| %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</dt>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% end %>
</fieldset>
<% end %>
</dl>
</section>
@@ -69,14 +77,16 @@ as well as a link to its edit page.
</div>
<% end %>
<% account_user_page.attributes.each do |attribute| -%>
<% if attribute.name == "user" %>
<%= f.hidden_field('user_id', value: page.resource.id) %>
<% else %>
<div class="field-unit field-unit--<%= attribute.html_class %> field-unit--<%= requireness(attribute) %>">
<%= render_field attribute, f: f %>
</div>
<% end %>
<% account_user_page.attributes.each do |title, attributes| -%>
<% attributes.each do |attribute| %>
<% if attribute.name == "user" %>
<%= f.hidden_field('user_id', value: page.resource.id) %>
<% else %>
<div class="field-unit field-unit--<%= attribute.html_class %> field-unit--<%= requireness(attribute) %>">
<%= render_field attribute, f: f %>
</div>
<% end %>
<% end %>
<% end -%>
<div class="form-actions">
@@ -0,0 +1,5 @@
class AddContactTypeToContacts < ActiveRecord::Migration[7.0]
def change
add_column :contacts, :contact_type, :integer, default: 0
end
end
@@ -0,0 +1,6 @@
class AddMiddleNameAndLastNameToContacts < ActiveRecord::Migration[7.0]
def change
add_column :contacts, :middle_name, :string, default: ''
add_column :contacts, :last_name, :string, default: ''
end
end
+4 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_12_23_040257) do
ActiveRecord::Schema[7.0].define(version: 2024_01_24_084032) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -418,6 +418,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_23_040257) do
t.string "identifier"
t.jsonb "custom_attributes", default: {}
t.datetime "last_activity_at", precision: nil
t.integer "contact_type", default: 0
t.string "middle_name", default: ""
t.string "last_name", default: ""
t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id"
t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
t.index ["account_id"], name: "index_contacts_on_account_id"
@@ -36,9 +36,11 @@ RSpec.describe 'Contacts API', type: :request do
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].first['email']).to eq(contact.email)
expect(response_body['payload'].first['contact_inboxes'].first['source_id']).to eq(contact_inbox.source_id)
expect(response_body['payload'].first['contact_inboxes'].first['inbox']['name']).to eq(contact_inbox.inbox.name)
contact_emails = response_body['payload'].pluck('email')
contact_inboxes_source_ids = response_body['payload'].flat_map { |c| c['contact_inboxes'].pluck('source_id') }
expect(contact_emails).to include(contact.email)
expect(contact_inboxes_source_ids).to include(contact_inbox.source_id)
end
it 'returns all contacts without contact inboxes' do
+22 -13
View File
@@ -4,16 +4,6 @@ RSpec.describe MutexApplicationJob do
let(:lock_manager) { instance_double(Redis::LockManager) }
let(:lock_key) { 'test_key' }
let(:test_mutex_job_class) do
stub_const('TestMutexJob', Class.new(MutexApplicationJob) do
def perform
with_lock('test_key') do
# Do nothing
end
end
end)
end
before do
allow(Redis::LockManager).to receive(:new).and_return(lock_manager)
allow(lock_manager).to receive(:lock).and_return(true)
@@ -22,24 +12,43 @@ RSpec.describe MutexApplicationJob do
describe '#with_lock' do
it 'acquires the lock and yields the block if lock is not acquired' do
expect(lock_manager).to receive(:lock).with(lock_key).and_return(true)
expect(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(true)
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
expect { |b| described_class.new.send(:with_lock, lock_key, &b) }.to yield_control
end
it 'acquires the lock with custom timeout' do
expect(lock_manager).to receive(:lock).with(lock_key, 5.seconds).and_return(true)
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
expect { |b| described_class.new.send(:with_lock, lock_key, 5.seconds, &b) }.to yield_control
end
it 'raises LockAcquisitionError if it cannot acquire the lock' do
allow(lock_manager).to receive(:lock).with(lock_key).and_return(false)
allow(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(false)
expect do
described_class.new.send(:with_lock, lock_key) do
# Do nothing
end
end.to raise_error(MutexApplicationJob::LockAcquisitionError)
expect(lock_manager).not_to receive(:unlock)
end
it 'raises StandardError if it execution raises it' do
allow(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(false)
allow(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
expect do
described_class.new.send(:with_lock, lock_key) do
raise StandardError
end
end.to raise_error(StandardError)
end
it 'ensures that the lock is released even if there is an error during block execution' do
expect(lock_manager).to receive(:lock).with(lock_key).and_return(true)
expect(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(true)
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
expect do
@@ -146,6 +146,61 @@ RSpec.describe Webhooks::WhatsappEventsJob do
end.not_to change(Message, :count)
end
it 'ignore reaction type message, would not create contact if the reaction is the first event' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
messages: [{
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
}],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Contact, :count)
end
it 'ignore request_welcome type message, would not create contact or conversation' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
messages: [{
from: '1111981136571', timestamp: '1664799904', type: 'request_welcome'
}],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Contact, :count)
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Conversation, :count)
end
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
@@ -81,7 +81,7 @@ describe Whatsapp::IncomingMessageService do
end
context 'when unsupported message types' do
it 'ignores type ephemeral' do
it 'ignores type ephemeral and does not create ghost conversation' do
params = {
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => '2423423243' }],
'messages' => [{ 'from' => '2423423243', 'id' => 'SDFADSf23sfasdafasdfa', 'text' => { 'body' => 'Test' },
@@ -89,12 +89,12 @@ describe Whatsapp::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(whatsapp_channel.inbox.conversations.count).to eq(0)
expect(Contact.count).to eq(0)
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
it 'ignores type unsupported' do
it 'ignores type unsupported and does not create ghost conversation' do
params = {
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => '2423423243' }],
'messages' => [{
@@ -105,8 +105,8 @@ describe Whatsapp::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(whatsapp_channel.inbox.conversations.count).to eq(0)
expect(Contact.count).to eq(0)
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
end