diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js
index fb2c8b89f..561695d28 100644
--- a/app/javascript/dashboard/store/modules/accounts.js
+++ b/app/javascript/dashboard/store/modules/accounts.js
@@ -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 {
diff --git a/app/javascript/dashboard/store/modules/specs/account/actions.spec.js b/app/javascript/dashboard/store/modules/specs/account/actions.spec.js
index 92f1328a5..57b4a2f80 100644
--- a/app/javascript/dashboard/store/modules/specs/account/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/account/actions.spec.js
@@ -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 }],
+ ]);
+ });
+ });
});
diff --git a/app/jobs/account/contacts_export_job.rb b/app/jobs/account/contacts_export_job.rb
index 778542b3c..33edcaa34 100644
--- a/app/jobs/account/contacts_export_job.rb
+++ b/app/jobs/account/contacts_export_job.rb
@@ -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
diff --git a/app/jobs/data_import_job.rb b/app/jobs/data_import_job.rb
index 9703d2e50..6146336fa 100644
--- a/app/jobs/data_import_job.rb
+++ b/app/jobs/data_import_job.rb
@@ -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
diff --git a/app/mailers/administrator_notifications/account_notification_mailer.rb b/app/mailers/administrator_notifications/account_notification_mailer.rb
new file mode 100644
index 000000000..8837e4f8c
--- /dev/null
+++ b/app/mailers/administrator_notifications/account_notification_mailer.rb
@@ -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
diff --git a/app/mailers/administrator_notifications/base_mailer.rb b/app/mailers/administrator_notifications/base_mailer.rb
new file mode 100644
index 000000000..2cedddbdb
--- /dev/null
+++ b/app/mailers/administrator_notifications/base_mailer.rb
@@ -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
diff --git a/app/mailers/administrator_notifications/channel_notifications_mailer.rb b/app/mailers/administrator_notifications/channel_notifications_mailer.rb
index dc4e6d7fe..e884b3df9 100644
--- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb
+++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb
@@ -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
diff --git a/app/mailers/administrator_notifications/integrations_notification_mailer.rb b/app/mailers/administrator_notifications/integrations_notification_mailer.rb
new file mode 100644
index 000000000..05477eca1
--- /dev/null
+++ b/app/mailers/administrator_notifications/integrations_notification_mailer.rb
@@ -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
diff --git a/app/models/account.rb b/app/models/account.rb
index eb95194c5..1cd59e1a4 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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')
diff --git a/app/models/concerns/reauthorizable.rb b/app/models/concerns/reauthorizable.rb
index b6792ebf1..32de1a8ef 100644
--- a/app/models/concerns/reauthorizable.rb
+++ b/app/models/concerns/reauthorizable.rb
@@ -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)
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index 5eb80c1ab..61e02ae77 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -26,4 +26,8 @@ class AccountPolicy < ApplicationPolicy
def checkout?
@account_user.administrator?
end
+
+ def toggle_deletion?
+ @account_user.administrator?
+ end
end
diff --git a/app/views/api/v1/models/_account.json.jbuilder b/app/views/api/v1/models/_account.json.jbuilder
index 5e9d9048a..52a199167 100644
--- a/app/views/api/v1/models/_account.json.jbuilder
+++ b/app/views/api/v1/models/_account.json.jbuilder
@@ -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
diff --git a/app/views/mailers/administrator_notifications/account_notification_mailer/account_deletion.liquid b/app/views/mailers/administrator_notifications/account_notification_mailer/account_deletion.liquid
new file mode 100644
index 000000000..0873dbac1
--- /dev/null
+++ b/app/views/mailers/administrator_notifications/account_notification_mailer/account_deletion.liquid
@@ -0,0 +1,16 @@
+
Hello,
+
+
Your account {{ meta.account_name }} has been marked for deletion. The account will be permanently deleted on {{ meta.deletion_date }}.
+
+{% if meta.reason == 'manual_deletion' %}
+
This action was requested by one of the administrators of your account.
+{% else %}
+
Reason for deletion: {{ meta.reason }}
+{% endif %}
+
+
If this was done in error, you can cancel the deletion process by visiting your account settings.
+
+
Cancel Account Deletion
+
+
Thank you,
+Team Chatwoot
\ No newline at end of file
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/automation_rule_disabled.liquid b/app/views/mailers/administrator_notifications/account_notification_mailer/automation_rule_disabled.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/automation_rule_disabled.liquid
rename to app/views/mailers/administrator_notifications/account_notification_mailer/automation_rule_disabled.liquid
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_export_complete.liquid b/app/views/mailers/administrator_notifications/account_notification_mailer/contact_export_complete.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_export_complete.liquid
rename to app/views/mailers/administrator_notifications/account_notification_mailer/contact_export_complete.liquid
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_complete.liquid b/app/views/mailers/administrator_notifications/account_notification_mailer/contact_import_complete.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_complete.liquid
rename to app/views/mailers/administrator_notifications/account_notification_mailer/contact_import_complete.liquid
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_failed.liquid b/app/views/mailers/administrator_notifications/account_notification_mailer/contact_import_failed.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_failed.liquid
rename to app/views/mailers/administrator_notifications/account_notification_mailer/contact_import_failed.liquid
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/dialogflow_disconnect.liquid b/app/views/mailers/administrator_notifications/integrations_notification_mailer/dialogflow_disconnect.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/dialogflow_disconnect.liquid
rename to app/views/mailers/administrator_notifications/integrations_notification_mailer/dialogflow_disconnect.liquid
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/slack_disconnect.liquid b/app/views/mailers/administrator_notifications/integrations_notification_mailer/slack_disconnect.liquid
similarity index 100%
rename from app/views/mailers/administrator_notifications/channel_notifications_mailer/slack_disconnect.liquid
rename to app/views/mailers/administrator_notifications/integrations_notification_mailer/slack_disconnect.liquid
diff --git a/config/routes.rb b/config/routes.rb
index 5bc965337..87344924d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -365,6 +365,7 @@ Rails.application.routes.draw do
post :checkout
post :subscription
get :limits
+ post :toggle_deletion
end
end
end
diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
index 86ec2fb55..70f1d177d 100644
--- a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
+++ b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
@@ -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
diff --git a/enterprise/app/models/enterprise/account.rb b/enterprise/app/models/enterprise/account.rb
index a1b0d0449..37bffc5a6 100644
--- a/enterprise/app/models/enterprise/account.rb
+++ b/enterprise/app/models/enterprise/account.rb
@@ -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
diff --git a/enterprise/app/models/enterprise/account/plan_usage_and_limits.rb b/enterprise/app/models/enterprise/account/plan_usage_and_limits.rb
new file mode 100644
index 000000000..ce03efa41
--- /dev/null
+++ b/enterprise/app/models/enterprise/account/plan_usage_and_limits.rb
@@ -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
diff --git a/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb b/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
index ac26dc525..0bd917c06 100644
--- a/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
+++ b/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
@@ -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
diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb
index e36a7217d..4d851d50e 100644
--- a/spec/enterprise/models/account_spec.rb
+++ b/spec/enterprise/models/account_spec.rb
@@ -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
diff --git a/spec/jobs/account/contacts_export_job_spec.rb b/spec/jobs/account/contacts_export_job_spec.rb
index 7c1858d0a..e6d3fda6a 100644
--- a/spec/jobs/account/contacts_export_job_spec.rb
+++ b/spec/jobs/account/contacts_export_job_spec.rb
@@ -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, [], {})
diff --git a/spec/mailers/administrator_notifications/account_notification_mailer_spec.rb b/spec/mailers/administrator_notifications/account_notification_mailer_spec.rb
new file mode 100644
index 000000000..44df38e88
--- /dev/null
+++ b/spec/mailers/administrator_notifications/account_notification_mailer_spec.rb
@@ -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
diff --git a/spec/mailers/administrator_notifications/base_mailer_spec.rb b/spec/mailers/administrator_notifications/base_mailer_spec.rb
new file mode 100644
index 000000000..619fef0a7
--- /dev/null
+++ b/spec/mailers/administrator_notifications/base_mailer_spec.rb
@@ -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
diff --git a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb
index 944475fb2..1be1314da 100644
--- a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb
+++ b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb
@@ -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
diff --git a/spec/mailers/administrator_notifications/integrations_notification_mailer_spec.rb b/spec/mailers/administrator_notifications/integrations_notification_mailer_spec.rb
new file mode 100644
index 000000000..331d33d06
--- /dev/null
+++ b/spec/mailers/administrator_notifications/integrations_notification_mailer_spec.rb
@@ -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
diff --git a/spec/mailers/administrator_notifications/shared/smtp_config_shared.rb b/spec/mailers/administrator_notifications/shared/smtp_config_shared.rb
new file mode 100644
index 000000000..96d4dbb0d
--- /dev/null
+++ b/spec/mailers/administrator_notifications/shared/smtp_config_shared.rb
@@ -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
diff --git a/spec/models/concerns/reauthorizable_shared.rb b/spec/models/concerns/reauthorizable_shared.rb
index 9efe232e8..a71800267 100644
--- a/spec/models/concerns/reauthorizable_shared.rb
+++ b/spec/models/concerns/reauthorizable_shared.rb
@@ -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!