mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 02:32:29 +00:00
feat: Ability to delete account for administrators (#1874)
## Description Add account delete option in the user account settings. Fixes #1555 ## Type of change - [ ] New feature (non-breaking change which adds functionality)   ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Sojan Jose <sojan.official@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -81,7 +81,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
COLLECTION_FILTERS = {
|
||||
active: ->(resources) { resources.where(status: :active) },
|
||||
suspended: ->(resources) { resources.where(status: :suspended) },
|
||||
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
|
||||
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) },
|
||||
marked_for_deletion: ->(resources) { resources.where("custom_attributes->>'marked_for_deletion_at' IS NOT NULL") }
|
||||
}.freeze
|
||||
|
||||
# Overwrite this method to customize how accounts are displayed
|
||||
|
||||
@@ -17,6 +17,12 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
getLimits() {
|
||||
return axios.get(`${this.url}limits`);
|
||||
}
|
||||
|
||||
toggleDeletion(action) {
|
||||
return axios.post(`${this.url}toggle_deletion`, {
|
||||
action_type: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('#enterpriseAccountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('update');
|
||||
expect(accountAPI).toHaveProperty('delete');
|
||||
expect(accountAPI).toHaveProperty('checkout');
|
||||
expect(accountAPI).toHaveProperty('toggleDeletion');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
@@ -42,5 +43,21 @@ describe('#enterpriseAccountAPI', () => {
|
||||
'/enterprise/api/v1/subscription'
|
||||
);
|
||||
});
|
||||
|
||||
it('#toggleDeletion with delete action', () => {
|
||||
accountAPI.toggleDeletion('delete');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/toggle_deletion',
|
||||
{ action_type: 'delete' }
|
||||
);
|
||||
});
|
||||
|
||||
it('#toggleDeletion with undelete action', () => {
|
||||
accountAPI.toggleDeletion('undelete');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/toggle_deletion',
|
||||
{ action_type: 'undelete' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
"ERROR": "Could not update settings, try again!",
|
||||
"SUCCESS": "Successfully updated account settings"
|
||||
},
|
||||
"ACCOUNT_DELETE_SECTION": {
|
||||
"TITLE": "Delete your Account",
|
||||
"NOTE": "Once you delete your account, all your data will be deleted.",
|
||||
"BUTTON_TEXT": "Delete Your Account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Delete Account",
|
||||
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"DISMISS": "Cancel",
|
||||
"PLACE_HOLDER": "Please type {accountName} to confirm"
|
||||
},
|
||||
"SUCCESS": "Account marked for deletion",
|
||||
"FAILURE": "Could not delete account, try again!",
|
||||
"SCHEDULED_DELETION": {
|
||||
"TITLE": "Account Scheduled for Deletion",
|
||||
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
|
||||
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
|
||||
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"ERROR": "Please fix form errors",
|
||||
"GENERAL_SECTION": {
|
||||
|
||||
@@ -11,11 +11,15 @@ import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WootConfirmDeleteModal from 'dashboard/components/widgets/modal/ConfirmDeleteModal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseSettingsHeader,
|
||||
V4Button,
|
||||
WootConfirmDeleteModal,
|
||||
NextButton,
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
@@ -35,6 +39,7 @@ export default {
|
||||
features: {},
|
||||
autoResolveDuration: null,
|
||||
latestChatwootVersion: null,
|
||||
showDeletePopup: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
@@ -55,6 +60,7 @@ export default {
|
||||
getAccount: 'accounts/getAccount',
|
||||
uiFlags: 'accounts/getUIFlags',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
}),
|
||||
showAutoResolutionConfig() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
@@ -101,6 +107,34 @@ export default {
|
||||
getAccountId() {
|
||||
return this.id.toString();
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t(
|
||||
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.PLACE_HOLDER',
|
||||
{
|
||||
accountName: this.name,
|
||||
}
|
||||
)}`;
|
||||
},
|
||||
isMarkedForDeletion() {
|
||||
const { custom_attributes = {} } = this.currentAccount;
|
||||
return !!custom_attributes.marked_for_deletion_at;
|
||||
},
|
||||
markedForDeletionDate() {
|
||||
const { custom_attributes = {} } = this.currentAccount;
|
||||
if (!custom_attributes.marked_for_deletion_at) return null;
|
||||
return new Date(custom_attributes.marked_for_deletion_at);
|
||||
},
|
||||
markedForDeletionReason() {
|
||||
const { custom_attributes = {} } = this.currentAccount;
|
||||
return custom_attributes.marked_for_deletion_reason || 'manual_deletion';
|
||||
},
|
||||
formattedDeletionDate() {
|
||||
if (!this.markedForDeletionDate) return '';
|
||||
return this.markedForDeletionDate.toLocaleString();
|
||||
},
|
||||
currentAccount() {
|
||||
return this.getAccount(this.accountId) || {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializeAccount();
|
||||
@@ -162,6 +196,56 @@ export default {
|
||||
rtl_view: isRTLSupported,
|
||||
});
|
||||
},
|
||||
// Delete Function
|
||||
openDeletePopup() {
|
||||
this.showDeletePopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeletePopup = false;
|
||||
},
|
||||
async markAccountForDeletion() {
|
||||
this.closeDeletePopup();
|
||||
try {
|
||||
// Use the enterprise API to toggle deletion with delete action
|
||||
await this.$store.dispatch('accounts/toggleDeletion', {
|
||||
action_type: 'delete',
|
||||
});
|
||||
// Refresh account data
|
||||
await this.$store.dispatch('accounts/get');
|
||||
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SUCCESS'));
|
||||
} catch (error) {
|
||||
// Handle error message
|
||||
this.handleDeletionError(error);
|
||||
}
|
||||
},
|
||||
handleDeletionError(error) {
|
||||
const errorKey = error.response?.data?.error_key;
|
||||
if (errorKey) {
|
||||
useAlert(
|
||||
this.$t(`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.${errorKey}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const message = error.response?.data?.message;
|
||||
if (message) {
|
||||
useAlert(message);
|
||||
return;
|
||||
}
|
||||
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.FAILURE'));
|
||||
},
|
||||
async clearDeletionMark() {
|
||||
try {
|
||||
// Use the enterprise API to toggle deletion with undelete action
|
||||
await this.$store.dispatch('accounts/toggleDeletion', {
|
||||
action_type: 'undelete',
|
||||
});
|
||||
// Refresh account data
|
||||
await this.$store.dispatch('accounts/get');
|
||||
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -175,7 +259,7 @@ export default {
|
||||
</V4Button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="flex-grow flex-shrink min-w-0 overflow-auto mt-3">
|
||||
<div class="flex-grow flex-shrink min-w-0 mt-3 overflow-auto">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex flex-row border-b border-slate-25 dark:border-slate-800"
|
||||
@@ -279,6 +363,73 @@ export default {
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
|
||||
<div
|
||||
class="flex flex-row pt-4 mt-2 border-t border-slate-25 dark:border-slate-800 text-black-900 dark:text-slate-300"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<div v-if="isMarkedForDeletion">
|
||||
<div
|
||||
class="p-4 flex-grow-0 flex-shrink-0 flex-[50%] bg-red-50 dark:bg-red-900 rounded"
|
||||
>
|
||||
<p class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.MESSAGE_${markedForDeletionReason === 'manual_deletion' ? 'MANUAL' : 'INACTIVITY'}`,
|
||||
{
|
||||
deletionDate: formattedDeletionDate,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<NextButton
|
||||
:label="
|
||||
$t(
|
||||
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.CLEAR_BUTTON'
|
||||
)
|
||||
"
|
||||
color="ruby"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="clearDeletionMark"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMarkedForDeletion">
|
||||
<NextButton
|
||||
:label="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.BUTTON_TEXT')"
|
||||
color="ruby"
|
||||
@click="openDeletePopup()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WootConfirmDeleteModal
|
||||
v-if="showDeletePopup"
|
||||
v-model:show="showDeletePopup"
|
||||
:title="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.TITLE')"
|
||||
:message="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.MESSAGE')"
|
||||
:confirm-text="
|
||||
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.BUTTON_TEXT')
|
||||
"
|
||||
:reject-text="
|
||||
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.DISMISS')
|
||||
"
|
||||
:confirm-value="name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="markAccountForDeletion"
|
||||
@on-close="closeDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
|
||||
@@ -73,6 +73,29 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, { id }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await AccountAPI.delete(id);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
toggleDeletion: async (
|
||||
{ commit },
|
||||
{ action_type } = { action_type: 'delete' }
|
||||
) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await EnterpriseAccountAPI.toggleDeletion(action_type);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
create: async ({ commit }, accountInfo) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
|
||||
@@ -80,4 +80,41 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleDeletion', () => {
|
||||
it('sends correct actions with delete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'delete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'delete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions with undelete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'undelete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'undelete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.toggleDeletion({ commit }, { action_type: 'delete' })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ class Account::ContactsExportJob < ApplicationJob
|
||||
|
||||
def send_mail
|
||||
file_url = account_contact_export_url
|
||||
mailer = AdministratorNotifications::ChannelNotificationsMailer.with(account: @account)
|
||||
mailer = AdministratorNotifications::AccountNotificationMailer.with(account: @account)
|
||||
mailer.contact_export_complete(file_url, @account_user.email)&.deliver_later
|
||||
end
|
||||
|
||||
|
||||
@@ -93,10 +93,10 @@ class DataImportJob < ApplicationJob
|
||||
end
|
||||
|
||||
def send_import_notification_to_admin
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_complete(@data_import).deliver_later
|
||||
AdministratorNotifications::AccountNotificationMailer.with(account: @data_import.account).contact_import_complete(@data_import).deliver_later
|
||||
end
|
||||
|
||||
def send_import_failed_notification_to_admin
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_failed.deliver_later
|
||||
AdministratorNotifications::AccountNotificationMailer.with(account: @data_import.account).contact_import_failed.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
class AdministratorNotifications::AccountNotificationMailer < AdministratorNotifications::BaseMailer
|
||||
def account_deletion(account, reason = 'manual_deletion')
|
||||
subject = 'Your account has been marked for deletion'
|
||||
action_url = settings_url('general')
|
||||
meta = {
|
||||
'account_name' => account.name,
|
||||
'deletion_date' => account.custom_attributes['marked_for_deletion_at'],
|
||||
'reason' => reason
|
||||
}
|
||||
|
||||
send_notification(subject, action_url: action_url, meta: meta)
|
||||
end
|
||||
|
||||
def contact_import_complete(resource)
|
||||
subject = 'Contact Import Completed'
|
||||
|
||||
action_url = if resource.failed_records.attached?
|
||||
Rails.application.routes.url_helpers.rails_blob_url(resource.failed_records)
|
||||
else
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{resource.account.id}/contacts"
|
||||
end
|
||||
|
||||
meta = {
|
||||
'failed_contacts' => resource.total_records - resource.processed_records,
|
||||
'imported_contacts' => resource.processed_records
|
||||
}
|
||||
|
||||
send_notification(subject, action_url: action_url, meta: meta)
|
||||
end
|
||||
|
||||
def contact_import_failed
|
||||
subject = 'Contact Import Failed'
|
||||
send_notification(subject)
|
||||
end
|
||||
|
||||
def contact_export_complete(file_url, email_to)
|
||||
subject = "Your contact's export file is available to download."
|
||||
send_notification(subject, to: email_to, action_url: file_url)
|
||||
end
|
||||
|
||||
def automation_rule_disabled(rule)
|
||||
subject = 'Automation rule disabled due to validation errors.'
|
||||
action_url = settings_url('automation/list')
|
||||
meta = { 'rule_name' => rule.name }
|
||||
|
||||
send_notification(subject, action_url: action_url, meta: meta)
|
||||
end
|
||||
end
|
||||
31
app/mailers/administrator_notifications/base_mailer.rb
Normal file
31
app/mailers/administrator_notifications/base_mailer.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class AdministratorNotifications::BaseMailer < ApplicationMailer
|
||||
# Common method to check SMTP configuration and send mail with liquid
|
||||
def send_notification(subject, to: nil, action_url: nil, meta: {})
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url = action_url
|
||||
@meta = meta || {}
|
||||
|
||||
send_mail_with_liquid(to: to || admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
# Helper method to generate inbox URL
|
||||
def inbox_url(inbox)
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/inboxes/#{inbox.id}"
|
||||
end
|
||||
|
||||
# Helper method to generate settings URL
|
||||
def settings_url(section)
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/#{section}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_emails
|
||||
Current.account.administrators.pluck(:email)
|
||||
end
|
||||
|
||||
def liquid_locals
|
||||
super.merge({ meta: @meta })
|
||||
end
|
||||
end
|
||||
@@ -1,93 +1,16 @@
|
||||
class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
|
||||
def slack_disconnect
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Your Slack integration has expired'
|
||||
@action_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/integrations/slack"
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def dialogflow_disconnect
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Your Dialogflow integration was disconnected'
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
class AdministratorNotifications::ChannelNotificationsMailer < AdministratorNotifications::BaseMailer
|
||||
def facebook_disconnect(inbox)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Your Facebook page connection has expired'
|
||||
@action_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/inboxes/#{inbox.id}"
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
send_notification(subject, action_url: inbox_url(inbox))
|
||||
end
|
||||
|
||||
def whatsapp_disconnect(inbox)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Your Whatsapp connection has expired'
|
||||
@action_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/inboxes/#{inbox.id}"
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
send_notification(subject, action_url: inbox_url(inbox))
|
||||
end
|
||||
|
||||
def email_disconnect(inbox)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Your email inbox has been disconnected. Please update the credentials for SMTP/IMAP'
|
||||
@action_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/inboxes/#{inbox.id}"
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_import_complete(resource)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Contact Import Completed'
|
||||
|
||||
@action_url = Rails.application.routes.url_helpers.rails_blob_url(resource.failed_records) if resource.failed_records.attached?
|
||||
@action_url ||= "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{resource.account.id}/contacts"
|
||||
@meta = {}
|
||||
@meta['failed_contacts'] = resource.total_records - resource.processed_records
|
||||
@meta['imported_contacts'] = resource.processed_records
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_import_failed
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
subject = 'Contact Import Failed'
|
||||
|
||||
@meta = {}
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_export_complete(file_url, email_to)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url = file_url
|
||||
subject = "Your contact's export file is available to download."
|
||||
|
||||
send_mail_with_liquid(to: email_to, subject: subject) and return
|
||||
end
|
||||
|
||||
def automation_rule_disabled(rule)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url ||= "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/settings/automation/list"
|
||||
|
||||
subject = 'Automation rule disabled due to validation errors.'.freeze
|
||||
@meta = {}
|
||||
@meta['rule_name'] = rule.name
|
||||
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_emails
|
||||
Current.account.administrators.pluck(:email)
|
||||
end
|
||||
|
||||
def liquid_locals
|
||||
super.merge({ meta: @meta })
|
||||
send_notification(subject, action_url: inbox_url(inbox))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class AdministratorNotifications::IntegrationsNotificationMailer < AdministratorNotifications::BaseMailer
|
||||
def slack_disconnect
|
||||
subject = 'Your Slack integration has expired'
|
||||
action_url = settings_url('integrations/slack')
|
||||
send_notification(subject, action_url: action_url)
|
||||
end
|
||||
|
||||
def dialogflow_disconnect
|
||||
subject = 'Your Dialogflow integration was disconnected'
|
||||
send_notification(subject)
|
||||
end
|
||||
end
|
||||
@@ -162,5 +162,6 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
Account.prepend_mod_with('Account')
|
||||
Account.prepend_mod_with('Account::PlanUsageAndLimits')
|
||||
Account.include_mod_with('Concerns::Account')
|
||||
Account.include_mod_with('Audit::Account')
|
||||
|
||||
@@ -39,33 +39,39 @@ module Reauthorizable
|
||||
def prompt_reauthorization!
|
||||
::Redis::Alfred.set(reauthorization_required_key, true)
|
||||
|
||||
mailer = AdministratorNotifications::ChannelNotificationsMailer.with(account: account)
|
||||
|
||||
case self.class.name
|
||||
when 'Integrations::Hook'
|
||||
process_integration_hook_reauthorization_emails(mailer)
|
||||
process_integration_hook_reauthorization_emails
|
||||
when 'Channel::FacebookPage'
|
||||
mailer.facebook_disconnect(inbox).deliver_later
|
||||
send_channel_reauthorization_email(:facebook_disconnect)
|
||||
when 'Channel::Whatsapp'
|
||||
mailer.whatsapp_disconnect(inbox).deliver_later
|
||||
send_channel_reauthorization_email(:whatsapp_disconnect)
|
||||
when 'Channel::Email'
|
||||
mailer.email_disconnect(inbox).deliver_later
|
||||
send_channel_reauthorization_email(:email_disconnect)
|
||||
when 'AutomationRule'
|
||||
update!(active: false)
|
||||
mailer.automation_rule_disabled(self).deliver_later
|
||||
handle_automation_rule_reauthorization
|
||||
end
|
||||
|
||||
invalidate_inbox_cache unless instance_of?(::AutomationRule)
|
||||
end
|
||||
|
||||
def process_integration_hook_reauthorization_emails(mailer)
|
||||
def process_integration_hook_reauthorization_emails
|
||||
if slack?
|
||||
mailer.slack_disconnect.deliver_later
|
||||
AdministratorNotifications::IntegrationsNotificationMailer.with(account: account).slack_disconnect.deliver_later
|
||||
elsif dialogflow?
|
||||
mailer.dialogflow_disconnect.deliver_later
|
||||
AdministratorNotifications::IntegrationsNotificationMailer.with(account: account).dialogflow_disconnect.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def send_channel_reauthorization_email(disconnect_type)
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).public_send(disconnect_type, inbox).deliver_later
|
||||
end
|
||||
|
||||
def handle_automation_rule_reauthorization
|
||||
update!(active: false)
|
||||
AdministratorNotifications::AccountNotificationMailer.with(account: account).automation_rule_disabled(self).deliver_later
|
||||
end
|
||||
|
||||
# call this after you successfully Reauthorized the object in UI
|
||||
def reauthorized!
|
||||
::Redis::Alfred.delete(authorization_error_count_key)
|
||||
|
||||
@@ -26,4 +26,8 @@ class AccountPolicy < ApplicationPolicy
|
||||
def checkout?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def toggle_deletion?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,10 @@ if resource.custom_attributes.present?
|
||||
json.timezone resource.custom_attributes['timezone'] if resource.custom_attributes['timezone'].present?
|
||||
json.logo resource.custom_attributes['logo'] if resource.custom_attributes['logo'].present?
|
||||
json.onboarding_step resource.custom_attributes['onboarding_step'] if resource.custom_attributes['onboarding_step'].present?
|
||||
json.marked_for_deletion_at resource.custom_attributes['marked_for_deletion_at'] if resource.custom_attributes['marked_for_deletion_at'].present?
|
||||
if resource.custom_attributes['marked_for_deletion_reason'].present?
|
||||
json.marked_for_deletion_reason resource.custom_attributes['marked_for_deletion_reason']
|
||||
end
|
||||
end
|
||||
end
|
||||
json.domain @account.domain
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>Your account <strong>{{ meta.account_name }}</strong> has been marked for deletion. The account will be permanently deleted on <strong>{{ meta.deletion_date }}</strong>.</p>
|
||||
|
||||
{% if meta.reason == 'manual_deletion' %}
|
||||
<p>This action was requested by one of the administrators of your account.</p>
|
||||
{% else %}
|
||||
<p>Reason for deletion: {{ meta.reason }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p>If this was done in error, you can cancel the deletion process by visiting your account settings.</p>
|
||||
|
||||
<p><a href="{{ action_url }}">Cancel Account Deletion</a></p>
|
||||
|
||||
<p>Thank you,<br>
|
||||
Team Chatwoot</p>
|
||||
@@ -365,6 +365,7 @@ Rails.application.routes.draw do
|
||||
post :checkout
|
||||
post :subscription
|
||||
get :limits
|
||||
post :toggle_deletion
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
include BillingHelper
|
||||
before_action :fetch_account
|
||||
before_action :check_authorization
|
||||
before_action :check_cloud_env, only: [:limits]
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
||||
|
||||
def subscription
|
||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||
@@ -42,13 +42,26 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
render_invalid_billing_details
|
||||
end
|
||||
|
||||
def toggle_deletion
|
||||
action_type = params[:action_type]
|
||||
|
||||
case action_type
|
||||
when 'delete'
|
||||
mark_for_deletion
|
||||
when 'undelete'
|
||||
unmark_for_deletion
|
||||
else
|
||||
render json: { error: 'Invalid action_type. Must be either "delete" or "undelete"' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_cloud_env
|
||||
installation_config = InstallationConfig.find_by(name: 'DEPLOYMENT_ENV')
|
||||
render json: { error: 'Not found' }, status: :not_found unless installation_config&.value == 'cloud'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_limits
|
||||
{
|
||||
'conversation' => {},
|
||||
@@ -67,6 +80,24 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
@account.custom_attributes['stripe_customer_id']
|
||||
end
|
||||
|
||||
def mark_for_deletion
|
||||
reason = 'manual_deletion'
|
||||
|
||||
if @account.mark_for_deletion(reason)
|
||||
render json: { message: 'Account marked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def unmark_for_deletion
|
||||
if @account.unmark_for_deletion
|
||||
render json: { message: 'Account unmarked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def render_invalid_billing_details
|
||||
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
||||
end
|
||||
|
||||
@@ -1,130 +1,14 @@
|
||||
module Enterprise::Account
|
||||
CAPTAIN_RESPONSES = 'captain_responses'.freeze
|
||||
CAPTAIN_DOCUMENTS = 'captain_documents'.freeze
|
||||
CAPTAIN_RESPONSES_USAGE = 'captain_responses_usage'.freeze
|
||||
CAPTAIN_DOCUMENTS_USAGE = 'captain_documents_usage'.freeze
|
||||
def mark_for_deletion(reason = 'manual_deletion')
|
||||
result = custom_attributes.merge!('marked_for_deletion_at' => 7.days.from_now.iso8601, 'marked_for_deletion_reason' => reason) && save
|
||||
|
||||
def usage_limits
|
||||
{
|
||||
agents: agent_limits.to_i,
|
||||
inboxes: get_limits(:inboxes).to_i,
|
||||
captain: {
|
||||
documents: get_captain_limits(:documents),
|
||||
responses: get_captain_limits(:responses)
|
||||
}
|
||||
}
|
||||
# Send notification to admin users if the account was successfully marked for deletion
|
||||
AdministratorNotifications::AccountNotificationMailer.with(account: self).account_deletion(self, reason).deliver_later if result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def increment_response_usage
|
||||
current_usage = custom_attributes[CAPTAIN_RESPONSES_USAGE].to_i || 0
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE] = current_usage + 1
|
||||
save
|
||||
end
|
||||
|
||||
def reset_response_usage
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE] = 0
|
||||
save
|
||||
end
|
||||
|
||||
def update_document_usage
|
||||
# this will ensure that the document count is always accurate
|
||||
custom_attributes[CAPTAIN_DOCUMENTS_USAGE] = captain_documents.count
|
||||
save
|
||||
end
|
||||
|
||||
def subscribed_features
|
||||
plan_features = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLAN_FEATURES')&.value
|
||||
return [] if plan_features.blank?
|
||||
|
||||
plan_features[plan_name]
|
||||
end
|
||||
|
||||
def captain_monthly_limit
|
||||
default_limits = default_captain_limits
|
||||
|
||||
{
|
||||
documents: self[:limits][CAPTAIN_DOCUMENTS] || default_limits['documents'],
|
||||
responses: self[:limits][CAPTAIN_RESPONSES] || default_limits['responses']
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_captain_limits(type)
|
||||
total_count = captain_monthly_limit[type.to_s].to_i
|
||||
|
||||
consumed = if type == :documents
|
||||
custom_attributes[CAPTAIN_DOCUMENTS_USAGE].to_i || 0
|
||||
else
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE].to_i || 0
|
||||
end
|
||||
|
||||
consumed = 0 if consumed.negative?
|
||||
|
||||
{
|
||||
total_count: total_count,
|
||||
current_available: (total_count - consumed).clamp(0, total_count),
|
||||
consumed: consumed
|
||||
}
|
||||
end
|
||||
|
||||
def default_captain_limits
|
||||
max_limits = { documents: ChatwootApp.max_limit, responses: ChatwootApp.max_limit }.with_indifferent_access
|
||||
zero_limits = { documents: 0, responses: 0 }.with_indifferent_access
|
||||
plan_quota = InstallationConfig.find_by(name: 'CAPTAIN_CLOUD_PLAN_LIMITS')&.value
|
||||
|
||||
# If there are no limits configured, we allow max usage
|
||||
return max_limits if plan_quota.blank?
|
||||
|
||||
# if there is plan_quota configred, but plan_name is not present, we return zero limits
|
||||
return zero_limits if plan_name.blank?
|
||||
|
||||
begin
|
||||
# Now we parse the plan_quota and return the limits for the plan name
|
||||
# but if there's no plan_name present in the plan_quota, we return zero limits
|
||||
plan_quota = JSON.parse(plan_quota) if plan_quota.present?
|
||||
plan_quota[plan_name.downcase] || zero_limits
|
||||
rescue StandardError
|
||||
# if there's any error in parsing the plan_quota, we return max limits
|
||||
# this is to ensure that we don't block the user from using the product
|
||||
max_limits
|
||||
end
|
||||
end
|
||||
|
||||
def plan_name
|
||||
custom_attributes['plan_name']
|
||||
end
|
||||
|
||||
def agent_limits
|
||||
subscribed_quantity = custom_attributes['subscribed_quantity']
|
||||
subscribed_quantity || get_limits(:agents)
|
||||
end
|
||||
|
||||
def get_limits(limit_name)
|
||||
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
|
||||
return self[:limits][limit_name.to_s] if self[:limits][limit_name.to_s].present?
|
||||
|
||||
return GlobalConfig.get(config_name)[config_name] if GlobalConfig.get(config_name)[config_name].present?
|
||||
|
||||
ChatwootApp.max_limit
|
||||
end
|
||||
|
||||
def validate_limit_keys
|
||||
errors.add(:limits, ': Invalid data') unless self[:limits].is_a? Hash
|
||||
self[:limits] = {} if self[:limits].blank?
|
||||
|
||||
limit_schema = {
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'inboxes' => { 'type': 'number' },
|
||||
'agents' => { 'type': 'number' },
|
||||
'captain_responses' => { 'type': 'number' },
|
||||
'captain_documents' => { 'type': 'number' }
|
||||
},
|
||||
'required' => [],
|
||||
'additionalProperties' => false
|
||||
}
|
||||
|
||||
errors.add(:limits, ': Invalid data') unless JSONSchemer.schema(limit_schema).valid?(self[:limits])
|
||||
def unmark_for_deletion
|
||||
custom_attributes.delete('marked_for_deletion_at') && custom_attributes.delete('marked_for_deletion_reason') && save
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
module Enterprise::Account::PlanUsageAndLimits
|
||||
CAPTAIN_RESPONSES = 'captain_responses'.freeze
|
||||
CAPTAIN_DOCUMENTS = 'captain_documents'.freeze
|
||||
CAPTAIN_RESPONSES_USAGE = 'captain_responses_usage'.freeze
|
||||
CAPTAIN_DOCUMENTS_USAGE = 'captain_documents_usage'.freeze
|
||||
|
||||
def usage_limits
|
||||
{
|
||||
agents: agent_limits.to_i,
|
||||
inboxes: get_limits(:inboxes).to_i,
|
||||
captain: {
|
||||
documents: get_captain_limits(:documents),
|
||||
responses: get_captain_limits(:responses)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def increment_response_usage
|
||||
current_usage = custom_attributes[CAPTAIN_RESPONSES_USAGE].to_i || 0
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE] = current_usage + 1
|
||||
save
|
||||
end
|
||||
|
||||
def reset_response_usage
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE] = 0
|
||||
save
|
||||
end
|
||||
|
||||
def update_document_usage
|
||||
# this will ensure that the document count is always accurate
|
||||
custom_attributes[CAPTAIN_DOCUMENTS_USAGE] = captain_documents.count
|
||||
save
|
||||
end
|
||||
|
||||
def subscribed_features
|
||||
plan_features = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLAN_FEATURES')&.value
|
||||
return [] if plan_features.blank?
|
||||
|
||||
plan_features[plan_name]
|
||||
end
|
||||
|
||||
def captain_monthly_limit
|
||||
default_limits = default_captain_limits
|
||||
|
||||
{
|
||||
documents: self[:limits][CAPTAIN_DOCUMENTS] || default_limits['documents'],
|
||||
responses: self[:limits][CAPTAIN_RESPONSES] || default_limits['responses']
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_captain_limits(type)
|
||||
total_count = captain_monthly_limit[type.to_s].to_i
|
||||
|
||||
consumed = if type == :documents
|
||||
custom_attributes[CAPTAIN_DOCUMENTS_USAGE].to_i || 0
|
||||
else
|
||||
custom_attributes[CAPTAIN_RESPONSES_USAGE].to_i || 0
|
||||
end
|
||||
|
||||
consumed = 0 if consumed.negative?
|
||||
|
||||
{
|
||||
total_count: total_count,
|
||||
current_available: (total_count - consumed).clamp(0, total_count),
|
||||
consumed: consumed
|
||||
}
|
||||
end
|
||||
|
||||
def default_captain_limits
|
||||
max_limits = { documents: ChatwootApp.max_limit, responses: ChatwootApp.max_limit }.with_indifferent_access
|
||||
zero_limits = { documents: 0, responses: 0 }.with_indifferent_access
|
||||
plan_quota = InstallationConfig.find_by(name: 'CAPTAIN_CLOUD_PLAN_LIMITS')&.value
|
||||
|
||||
# If there are no limits configured, we allow max usage
|
||||
return max_limits if plan_quota.blank?
|
||||
|
||||
# if there is plan_quota configred, but plan_name is not present, we return zero limits
|
||||
return zero_limits if plan_name.blank?
|
||||
|
||||
begin
|
||||
# Now we parse the plan_quota and return the limits for the plan name
|
||||
# but if there's no plan_name present in the plan_quota, we return zero limits
|
||||
plan_quota = JSON.parse(plan_quota) if plan_quota.present?
|
||||
plan_quota[plan_name.downcase] || zero_limits
|
||||
rescue StandardError
|
||||
# if there's any error in parsing the plan_quota, we return max limits
|
||||
# this is to ensure that we don't block the user from using the product
|
||||
max_limits
|
||||
end
|
||||
end
|
||||
|
||||
def plan_name
|
||||
custom_attributes['plan_name']
|
||||
end
|
||||
|
||||
def agent_limits
|
||||
subscribed_quantity = custom_attributes['subscribed_quantity']
|
||||
subscribed_quantity || get_limits(:agents)
|
||||
end
|
||||
|
||||
def get_limits(limit_name)
|
||||
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
|
||||
return self[:limits][limit_name.to_s] if self[:limits][limit_name.to_s].present?
|
||||
|
||||
return GlobalConfig.get(config_name)[config_name] if GlobalConfig.get(config_name)[config_name].present?
|
||||
|
||||
ChatwootApp.max_limit
|
||||
end
|
||||
|
||||
def validate_limit_keys
|
||||
errors.add(:limits, ': Invalid data') unless self[:limits].is_a? Hash
|
||||
self[:limits] = {} if self[:limits].blank?
|
||||
|
||||
limit_schema = {
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'inboxes' => { 'type': 'number' },
|
||||
'agents' => { 'type': 'number' },
|
||||
'captain_responses' => { 'type': 'number' },
|
||||
'captain_documents' => { 'type': 'number' }
|
||||
},
|
||||
'required' => [],
|
||||
'additionalProperties' => false
|
||||
}
|
||||
|
||||
errors.add(:limits, ': Invalid data') unless JSONSchemer.schema(limit_schema).valid?(self[:limits])
|
||||
end
|
||||
end
|
||||
@@ -241,4 +241,99 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /enterprise/api/v1/accounts/{account.id}/toggle_deletion' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion", as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deployment environment is not cloud' do
|
||||
before do
|
||||
# Set deployment environment to something other than cloud
|
||||
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'self_hosted')
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { action_type: 'delete' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(JSON.parse(response.body)['error']).to eq('Not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
before do
|
||||
# Create the installation config for cloud environment
|
||||
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
|
||||
end
|
||||
|
||||
it 'marks the account for deletion when action is delete' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { action_type: 'delete' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_present
|
||||
expect(account.custom_attributes['marked_for_deletion_reason']).to eq('manual_deletion')
|
||||
end
|
||||
|
||||
it 'unmarks the account for deletion when action is undelete' do
|
||||
# First mark the account for deletion
|
||||
account.update!(
|
||||
custom_attributes: {
|
||||
'marked_for_deletion_at' => 7.days.from_now.iso8601,
|
||||
'marked_for_deletion_reason' => 'manual_deletion'
|
||||
}
|
||||
)
|
||||
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { action_type: 'undelete' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_nil
|
||||
expect(account.custom_attributes['marked_for_deletion_reason']).to be_nil
|
||||
end
|
||||
|
||||
it 'returns error for invalid action' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { action_type: 'invalid' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['error']).to include('Invalid action_type')
|
||||
end
|
||||
|
||||
it 'returns error when action parameter is missing' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['error']).to include('Invalid action_type')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -221,4 +221,53 @@ RSpec.describe Account, type: :model do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'account deletion' do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
describe '#mark_for_deletion' do
|
||||
it 'sets the marked_for_deletion_at and marked_for_deletion_reason attributes' do
|
||||
expect do
|
||||
account.mark_for_deletion('test_reason')
|
||||
end.to change { account.reload.custom_attributes['marked_for_deletion_at'] }.from(nil).to(be_present)
|
||||
.and change { account.reload.custom_attributes['marked_for_deletion_reason'] }.from(nil).to('test_reason')
|
||||
end
|
||||
|
||||
it 'sends a notification email to admin users' do
|
||||
mailer = double
|
||||
expect(AdministratorNotifications::AccountNotificationMailer).to receive(:with).with(account: account).and_return(mailer)
|
||||
expect(mailer).to receive(:account_deletion).with(account, 'test_reason').and_return(mailer)
|
||||
expect(mailer).to receive(:deliver_later)
|
||||
|
||||
account.mark_for_deletion('test_reason')
|
||||
end
|
||||
|
||||
it 'returns true when successful' do
|
||||
expect(account.mark_for_deletion).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unmark_for_deletion' do
|
||||
before do
|
||||
account.update!(
|
||||
custom_attributes: {
|
||||
'marked_for_deletion_at' => 7.days.from_now.iso8601,
|
||||
'marked_for_deletion_reason' => 'test_reason'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes the marked_for_deletion_at and marked_for_deletion_reason attributes' do
|
||||
expect do
|
||||
account.unmark_for_deletion
|
||||
end.to change { account.reload.custom_attributes['marked_for_deletion_at'] }.from(be_present).to(nil)
|
||||
.and change { account.reload.custom_attributes['marked_for_deletion_reason'] }.from('test_reason').to(nil)
|
||||
end
|
||||
|
||||
it 'returns true when successful' do
|
||||
expect(account.unmark_for_deletion).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,7 +60,7 @@ RSpec.describe Account::ContactsExportJob do
|
||||
|
||||
it 'generates CSV file and attach to account' do
|
||||
mailer = double
|
||||
allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).with(account: account).and_return(mailer)
|
||||
allow(AdministratorNotifications::AccountNotificationMailer).to receive(:with).with(account: account).and_return(mailer)
|
||||
allow(mailer).to receive(:contact_export_complete)
|
||||
|
||||
described_class.perform_now(account.id, user.id, [], {})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/mailers/administrator_notifications/shared/smtp_config_shared.rb'
|
||||
|
||||
RSpec.describe AdministratorNotifications::AccountNotificationMailer do
|
||||
include_context 'with smtp config'
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
describe 'account_deletion' do
|
||||
let(:reason) { 'manual_deletion' }
|
||||
let(:mail) { described_class.with(account: account).account_deletion(account, reason) }
|
||||
let(:deletion_date) { 7.days.from_now.iso8601 }
|
||||
|
||||
before do
|
||||
account.update!(custom_attributes: {
|
||||
'marked_for_deletion_at' => deletion_date,
|
||||
'marked_for_deletion_reason' => reason
|
||||
})
|
||||
end
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your account has been marked for deletion')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([admin.email])
|
||||
end
|
||||
|
||||
it 'includes the account name in the email body' do
|
||||
expect(mail.body.encoded).to include(account.name)
|
||||
end
|
||||
|
||||
it 'includes the deletion date in the email body' do
|
||||
expect(mail.body.encoded).to include(deletion_date)
|
||||
end
|
||||
|
||||
it 'includes a link to cancel the deletion' do
|
||||
expect(mail.body.encoded).to include('Cancel Account Deletion')
|
||||
end
|
||||
|
||||
context 'when reason is manual_deletion' do
|
||||
it 'includes the administrator message' do
|
||||
expect(mail.body.encoded).to include('This action was requested by one of the administrators of your account')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reason is not manual_deletion' do
|
||||
let(:reason) { 'inactivity' }
|
||||
|
||||
it 'includes the reason directly' do
|
||||
expect(mail.body.encoded).to include('Reason for deletion: inactivity')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact_import_complete' do
|
||||
let!(:data_import) { build(:data_import, total_records: 10, processed_records: 8) }
|
||||
let(:mail) { described_class.with(account: account).contact_import_complete(data_import).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Contact Import Completed')
|
||||
end
|
||||
|
||||
it 'renders the processed records' do
|
||||
expect(mail.body.encoded).to include('Number of records imported: 8')
|
||||
expect(mail.body.encoded).to include('Number of records failed: 2')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([admin.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact_import_failed' do
|
||||
let(:mail) { described_class.with(account: account).contact_import_failed.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Contact Import Failed')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([admin.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact_export_complete' do
|
||||
let!(:file_url) { 'http://test.com/test' }
|
||||
let(:mail) { described_class.with(account: account).contact_export_complete(file_url, admin.email).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("Your contact's export file is available to download.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([admin.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'automation_rule_disabled' do
|
||||
let(:rule) { instance_double(AutomationRule, name: 'Test Rule') }
|
||||
let(:mail) { described_class.with(account: account).automation_rule_disabled(rule).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Automation rule disabled due to validation errors.')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([admin.email])
|
||||
end
|
||||
|
||||
it 'includes the rule name in the email body' do
|
||||
expect(mail.body.encoded).to include('Test Rule')
|
||||
end
|
||||
end
|
||||
end
|
||||
75
spec/mailers/administrator_notifications/base_mailer_spec.rb
Normal file
75
spec/mailers/administrator_notifications/base_mailer_spec.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AdministratorNotifications::BaseMailer do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
||||
let!(:admin2) { create(:user, account: account, role: :administrator) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:mailer) { described_class.new }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
Current.account = account
|
||||
end
|
||||
|
||||
describe 'admin_emails' do
|
||||
it 'returns emails of all administrators' do
|
||||
# Call the private method
|
||||
admin_emails = mailer.send(:admin_emails)
|
||||
|
||||
expect(admin_emails).to include(admin1.email)
|
||||
expect(admin_emails).to include(admin2.email)
|
||||
expect(admin_emails).not_to include(agent.email)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'helper methods' do
|
||||
it 'generates correct inbox URL' do
|
||||
url = mailer.inbox_url(inbox)
|
||||
expected_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/inboxes/#{inbox.id}"
|
||||
expect(url).to eq(expected_url)
|
||||
end
|
||||
|
||||
it 'generates correct settings URL' do
|
||||
url = mailer.settings_url('automation/list')
|
||||
expected_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/automation/list"
|
||||
expect(url).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'send_notification' do
|
||||
before do
|
||||
allow(mailer).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
end
|
||||
|
||||
it 'sends email with correct parameters' do
|
||||
subject = 'Test Subject'
|
||||
action_url = 'https://example.com'
|
||||
meta = { 'key' => 'value' }
|
||||
|
||||
# Mock the send_mail_with_liquid method
|
||||
expect(mailer).to receive(:send_mail_with_liquid).with(
|
||||
to: [admin1.email, admin2.email],
|
||||
subject: subject
|
||||
).and_return(true)
|
||||
|
||||
mailer.send_notification(subject, action_url: action_url, meta: meta)
|
||||
|
||||
# Check that instance variables are set correctly
|
||||
expect(mailer.instance_variable_get(:@action_url)).to eq(action_url)
|
||||
expect(mailer.instance_variable_get(:@meta)).to eq(meta)
|
||||
end
|
||||
|
||||
it 'uses provided email addresses when specified' do
|
||||
subject = 'Test Subject'
|
||||
custom_email = 'custom@example.com'
|
||||
|
||||
expect(mailer).to receive(:send_mail_with_liquid).with(
|
||||
to: custom_email,
|
||||
subject: subject
|
||||
).and_return(true)
|
||||
|
||||
mailer.send_notification(subject, to: custom_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/mailers/administrator_notifications/shared/smtp_config_shared.rb'
|
||||
|
||||
RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
include_context 'with smtp config'
|
||||
|
||||
let(:class_instance) { described_class.new }
|
||||
let!(:account) { create(:account) }
|
||||
let!(:administrator) { create(:user, :administrator, email: 'agent1@example.com', account: account) }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:new).and_return(class_instance)
|
||||
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
end
|
||||
|
||||
describe 'slack_disconnect' do
|
||||
let(:mail) { described_class.with(account: account).slack_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Slack integration has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'dialogflow disconnect' do
|
||||
let(:mail) { described_class.with(account: account).dialogflow_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Dialogflow integration was disconnected')
|
||||
end
|
||||
|
||||
it 'renders the content' do
|
||||
expect(mail.body).to include('Your Dialogflow integration was disconnected because of permission issues.')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'facebook_disconnect' do
|
||||
before do
|
||||
stub_request(:post, /graph.facebook.com/)
|
||||
@@ -47,14 +17,17 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
|
||||
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
|
||||
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
|
||||
let(:mail) { described_class.with(account: account).facebook_disconnect(facebook_inbox).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Facebook page connection has expired')
|
||||
end
|
||||
context 'when sending the actual email' do
|
||||
let(:mail) { described_class.with(account: account).facebook_disconnect(facebook_inbox).deliver_now }
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Facebook page connection has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,35 +44,4 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact_import_complete' do
|
||||
let!(:data_import) { build(:data_import, total_records: 10, processed_records: 10) }
|
||||
let(:mail) { described_class.with(account: account).contact_import_complete(data_import).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Contact Import Completed')
|
||||
end
|
||||
|
||||
it 'renders the processed records' do
|
||||
expect(mail.body.encoded).to match('Number of records imported: 10')
|
||||
expect(mail.body.encoded).to match('Number of records failed: 0')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact_export_complete' do
|
||||
let!(:file_url) { 'http://test.com/test' }
|
||||
let(:mail) { described_class.with(account: account).contact_export_complete(file_url, administrator.email).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("Your contact's export file is available to download.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/mailers/administrator_notifications/shared/smtp_config_shared.rb'
|
||||
|
||||
RSpec.describe AdministratorNotifications::IntegrationsNotificationMailer do
|
||||
include_context 'with smtp config'
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:administrator) { create(:user, :administrator, email: 'admin@example.com', account: account) }
|
||||
|
||||
describe 'slack_disconnect' do
|
||||
let(:mail) { described_class.with(account: account).slack_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Slack integration has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
|
||||
it 'includes reconnect instructions in the body' do
|
||||
expect(mail.body.encoded).to include('To continue receiving messages on Slack, please delete the integration and connect your workspace again')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'dialogflow_disconnect' do
|
||||
let(:mail) { described_class.with(account: account).dialogflow_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Dialogflow integration was disconnected')
|
||||
end
|
||||
|
||||
it 'renders the content' do
|
||||
expect(mail.body.encoded).to include('Your Dialogflow integration was disconnected because of permission issues')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([administrator.email])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'with smtp config' do
|
||||
before do
|
||||
# We need to use allow_any_instance_of here because smtp_config_set_or_development?
|
||||
# is defined in ApplicationMailer and needs to be stubbed for all mailer instances
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(ApplicationMailer).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,9 @@ require 'rails_helper'
|
||||
|
||||
shared_examples_for 'reauthorizable' do
|
||||
let(:model) { described_class } # the class that includes the concern
|
||||
let(:obj) { FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym) }
|
||||
|
||||
it 'authorization_error!' do
|
||||
obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym)
|
||||
expect(obj.authorization_error_count).to eq 0
|
||||
|
||||
obj.authorization_error!
|
||||
@@ -13,7 +13,6 @@ shared_examples_for 'reauthorizable' do
|
||||
end
|
||||
|
||||
it 'prompts reauthorization when error threshold is passed' do
|
||||
obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym)
|
||||
expect(obj.reauthorization_required?).to be false
|
||||
|
||||
obj.class::AUTHORIZATION_ERROR_THRESHOLD.times do
|
||||
@@ -23,25 +22,70 @@ shared_examples_for 'reauthorizable' do
|
||||
expect(obj.reauthorization_required?).to be true
|
||||
end
|
||||
|
||||
it 'prompt_reauthorization!' do
|
||||
obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym)
|
||||
mailer = double
|
||||
mailer_method = double
|
||||
allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).and_return(mailer)
|
||||
# allow mailer to receive any methods and return mailer
|
||||
allow(mailer).to receive(:method_missing).and_return(mailer_method)
|
||||
allow(mailer_method).to receive(:deliver_later)
|
||||
# Helper methods to set up mailer mocks
|
||||
def setup_automation_rule_mailer(_obj)
|
||||
account_mailer = instance_double(AdministratorNotifications::AccountNotificationMailer)
|
||||
automation_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
allow(AdministratorNotifications::AccountNotificationMailer).to receive(:with).and_return(account_mailer)
|
||||
allow(account_mailer).to receive(:automation_rule_disabled).and_return(automation_mailer_response)
|
||||
end
|
||||
|
||||
expect(obj.reauthorization_required?).to be false
|
||||
def setup_integrations_hook_mailer(obj)
|
||||
integrations_mailer = instance_double(AdministratorNotifications::IntegrationsNotificationMailer)
|
||||
slack_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
dialogflow_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
allow(AdministratorNotifications::IntegrationsNotificationMailer).to receive(:with).and_return(integrations_mailer)
|
||||
allow(integrations_mailer).to receive(:slack_disconnect).and_return(slack_mailer_response)
|
||||
allow(integrations_mailer).to receive(:dialogflow_disconnect).and_return(dialogflow_mailer_response)
|
||||
|
||||
obj.prompt_reauthorization!
|
||||
expect(obj.reauthorization_required?).to be true
|
||||
expect(AdministratorNotifications::ChannelNotificationsMailer).to have_received(:with).with(account: obj.account)
|
||||
expect(mailer_method).to have_received(:deliver_later)
|
||||
# Allow the model to respond to slack? and dialogflow? methods
|
||||
allow(obj).to receive(:slack?).and_return(true)
|
||||
allow(obj).to receive(:dialogflow?).and_return(false)
|
||||
end
|
||||
|
||||
def setup_channel_mailer(_obj)
|
||||
channel_mailer = instance_double(AdministratorNotifications::ChannelNotificationsMailer)
|
||||
facebook_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
whatsapp_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
email_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true)
|
||||
allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).and_return(channel_mailer)
|
||||
allow(channel_mailer).to receive(:facebook_disconnect).and_return(facebook_mailer_response)
|
||||
allow(channel_mailer).to receive(:whatsapp_disconnect).and_return(whatsapp_mailer_response)
|
||||
allow(channel_mailer).to receive(:email_disconnect).and_return(email_mailer_response)
|
||||
end
|
||||
|
||||
describe 'prompt_reauthorization!' do
|
||||
before do
|
||||
# Setup mailer mocks based on model type
|
||||
if model.to_s == 'AutomationRule'
|
||||
setup_automation_rule_mailer(obj)
|
||||
elsif model.to_s == 'Integrations::Hook'
|
||||
setup_integrations_hook_mailer(obj)
|
||||
else
|
||||
setup_channel_mailer(obj)
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets reauthorization required flag' do
|
||||
expect(obj.reauthorization_required?).to be false
|
||||
obj.prompt_reauthorization!
|
||||
expect(obj.reauthorization_required?).to be true
|
||||
end
|
||||
|
||||
it 'calls the correct mailer based on model type' do
|
||||
obj.prompt_reauthorization!
|
||||
|
||||
if model.to_s == 'AutomationRule'
|
||||
expect(AdministratorNotifications::AccountNotificationMailer).to have_received(:with).with(account: obj.account)
|
||||
elsif model.to_s == 'Integrations::Hook'
|
||||
expect(AdministratorNotifications::IntegrationsNotificationMailer).to have_received(:with).with(account: obj.account)
|
||||
else
|
||||
expect(AdministratorNotifications::ChannelNotificationsMailer).to have_received(:with).with(account: obj.account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'reauthorized!' do
|
||||
obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym)
|
||||
# setting up the object with the errors to validate its cleared on action
|
||||
obj.authorization_error!
|
||||
obj.prompt_reauthorization!
|
||||
|
||||
Reference in New Issue
Block a user