diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 221c96b85..eff9975f7 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -49,6 +49,11 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}" end end + + # This endpoint is used to bulk create agents during onboarding + # onboarding_step key in present in Current account custom attributes, since this is a one time operation + Current.account.custom_attributes.delete('onboarding_step') + Current.account.save! head :ok end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index f424f3b66..98683ea25 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def export column_names = params['column_names'] - Account::ContactsExportJob.perform_later(Current.account.id, column_names) + Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email) head :ok, message: I18n.t('errors.contacts.export.success') end diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 3ac0568e3..52035ce64 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -54,7 +54,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro end def snooze - @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until] + updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil) + @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until] render json: @notification end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 481594ee4..c0dac6d9e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController def update @account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration)) @account.custom_attributes.merge!(custom_attributes_params) + @account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update' @account.save! end diff --git a/app/controllers/api/v2/accounts_controller.rb b/app/controllers/api/v2/accounts_controller.rb index ea19727ad..45faca3d0 100644 --- a/app/controllers/api/v2/accounts_controller.rb +++ b/app/controllers/api/v2/accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V2::AccountsController < Api::BaseController ).perform fetch_account_and_user_info + update_account_info if @account.present? if @user send_auth_headers(@user) @@ -33,6 +34,18 @@ class Api::V2::AccountsController < Api::BaseController private + def account_attributes + { + custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' }) + } + end + + def update_account_info + @account.update!( + account_attributes + ) + end + def fetch_account_and_user_info; end def fetch_account diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9e59758ea..0aea9df83 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -55,7 +55,7 @@ class DashboardController < ActionController::Base VAPID_PUBLIC_KEY: VapidService.public_key, ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), - FACEBOOK_API_VERSION: 'v14.0', + FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index a31d01675..6223f7174 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -34,7 +34,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController def allowed_configs @allowed_configs = case @config when 'facebook' - %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 594e34c4f..cae908016 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -79,7 +79,13 @@ "TITLE": "Export Contacts", "DESC": "Export contacts to a CSV file.", "SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.", - "ERROR_MESSAGE": "There was an error, please try again" + "ERROR_MESSAGE": "There was an error, please try again", + "CONFIRM": { + "TITLE": "Export Contacts", + "MESSAGE": "Are you sure you want to export all contacts?", + "YES": "Yes, Export", + "NO": "No, Cancel" + } }, "DELETE_NOTE": { "CONFIRM": { diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue index 4df4017b2..1b4fd90e0 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue @@ -113,6 +113,13 @@ + @@ -175,8 +182,13 @@ export default { toggleImport() { this.$emit('on-toggle-import'); }, - submitExport() { - this.$emit('on-export-submit'); + async submitExport() { + const ok = + await this.$refs.confirmExportContactsDialog.showConfirmation(); + + if (ok) { + this.$emit('on-export-submit'); + } }, submitSearch() { this.$emit('on-search-submit'); diff --git a/app/jobs/account/contacts_export_job.rb b/app/jobs/account/contacts_export_job.rb index 0ae8b3892..b8e9dbc96 100644 --- a/app/jobs/account/contacts_export_job.rb +++ b/app/jobs/account/contacts_export_job.rb @@ -1,13 +1,13 @@ class Account::ContactsExportJob < ApplicationJob queue_as :low - def perform(account_id, column_names) + def perform(account_id, column_names, email_to) account = Account.find(account_id) headers = valid_headers(column_names) generate_csv(account, headers) file_url = account_contact_export_url(account) - AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url)&.deliver_later + AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url, email_to)&.deliver_later end def generate_csv(account, headers) diff --git a/app/jobs/notification/reopen_snoozed_notifications_job.rb b/app/jobs/notification/reopen_snoozed_notifications_job.rb index 05235b62b..91ed5ee61 100644 --- a/app/jobs/notification/reopen_snoozed_notifications_job.rb +++ b/app/jobs/notification/reopen_snoozed_notifications_job.rb @@ -2,9 +2,24 @@ class Notification::ReopenSnoozedNotificationsJob < ApplicationJob queue_as :low def perform - # rubocop:disable Rails/SkipsModelValidations - Notification.where(snoozed_until: 3.days.ago..Time.current) - .update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current, read_at: nil) - # rubocop:enable Rails/SkipsModelValidations + Notification.where(snoozed_until: 3.days.ago..Time.current).find_in_batches(batch_size: 100) do |notifications_batch| + notifications_batch.each do |notification| + update_notification(notification) + end + end + end + + private + + def update_notification(notification) + updated_meta = (notification.meta || {}).merge('last_snoozed_at' => notification.snoozed_until) + + notification.update!( + snoozed_until: nil, + updated_at: Time.current, + last_activity_at: Time.current, + meta: updated_meta, + read_at: nil + ) end end diff --git a/app/mailers/administrator_notifications/channel_notifications_mailer.rb b/app/mailers/administrator_notifications/channel_notifications_mailer.rb index 8c52a9bd3..6c0f7cee2 100644 --- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb +++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb @@ -60,12 +60,13 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer send_mail_with_liquid(to: admin_emails, subject: subject) and return end - def contact_export_complete(file_url) + 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: admin_emails, subject: subject) and return + + send_mail_with_liquid(to: email_to, subject: subject) and return end private diff --git a/app/models/notification.rb b/app/models/notification.rb index f8c6e3d5b..113c6cf3e 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -64,24 +64,17 @@ class Notification < ApplicationRecord created_at: created_at.to_i, last_activity_at: last_activity_at.to_i, snoozed_until: snoozed_until, + meta: meta, account_id: account_id } if primary_actor.present? - payload[:primary_actor] = primary_actor_data + payload[:primary_actor] = primary_actor&.push_event_data payload[:push_message_title] = push_message_title end payload end - def primary_actor_data - { - id: primary_actor.push_event_data[:id], - meta: primary_actor.push_event_data[:meta], - inbox_id: primary_actor.push_event_data[:inbox_id] - } - end - def fcm_push_data { id: id, diff --git a/app/views/api/v1/accounts/notifications/index.json.jbuilder b/app/views/api/v1/accounts/notifications/index.json.jbuilder index ae86372c9..4a408dfca 100644 --- a/app/views/api/v1/accounts/notifications/index.json.jbuilder +++ b/app/views/api/v1/accounts/notifications/index.json.jbuilder @@ -21,6 +21,7 @@ json.data do json.created_at notification.created_at.to_i json.last_activity_at notification.last_activity_at.to_i json.snoozed_until notification.snoozed_until + json.meta notification.meta end end end diff --git a/app/views/api/v1/models/_account.json.jbuilder b/app/views/api/v1/models/_account.json.jbuilder index 9f89a5510..5e9d9048a 100644 --- a/app/views/api/v1/models/_account.json.jbuilder +++ b/app/views/api/v1/models/_account.json.jbuilder @@ -6,6 +6,11 @@ if resource.custom_attributes.present? json.subscribed_quantity resource.custom_attributes['subscribed_quantity'] json.subscription_status resource.custom_attributes['subscription_status'] json.subscription_ends_on resource.custom_attributes['subscription_ends_on'] + json.industry resource.custom_attributes['industry'] if resource.custom_attributes['industry'].present? + json.company_size resource.custom_attributes['company_size'] if resource.custom_attributes['company_size'].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? end end json.domain @account.domain diff --git a/config/installation_config.yml b/config/installation_config.yml index 60eae3bc2..9db527832 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -110,6 +110,11 @@ display_title: 'Instagram Verify Token' description: 'The verify token used for Instagram Webhook' locked: false +- name: FACEBOOK_API_VERSION + display_title: 'Facebook API Version' + description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`' + value: 'v17.0' + locked: false - name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT display_title: 'Enable human agent' value: false diff --git a/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb index d0d76c1ae..3edefbb5f 100644 --- a/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb +++ b/enterprise/app/controllers/enterprise/api/v2/accounts_controller.rb @@ -2,12 +2,11 @@ module Enterprise::Api::V2::AccountsController private def fetch_account_and_user_info - data = fetch_from_clearbit + @data = fetch_from_clearbit - return if data.blank? + return if @data.blank? - update_user_info(data) - update_account_info(data) + update_user_info end def fetch_from_clearbit @@ -17,19 +16,25 @@ module Enterprise::Api::V2::AccountsController nil end - def update_user_info(data) - @user.update!(name: data[:name]) + def update_user_info + @user.update!(name: @data[:name]) if @data[:name].present? end - def update_account_info(data) - @account.update!( - name: data[:company_name], - custom_attributes: @account.custom_attributes.merge( - 'industry' => data[:industry], - 'company_size' => data[:company_size], - 'timezone' => data[:timezone], - 'logo' => data[:logo] - ) + def data_from_clearbit + return {} if @data.blank? + + { name: @data[:company_name], + custom_attributes: { + 'industry' => @data[:industry], + 'company_size' => @data[:company_size], + 'timezone' => @data[:timezone], + 'logo' => @data[:logo] + } } + end + + def account_attributes + super.deep_merge( + data_from_clearbit ) end end diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index e5e298747..21511310b 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -197,7 +197,7 @@ RSpec.describe 'Contacts API', type: :request do let(:admin) { create(:user, account: account, role: :administrator) } it 'enqueues a contact export job' do - expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil).once + expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil, admin.email).once get "/api/v1/accounts/#{account.id}/contacts/export", headers: admin.create_new_auth_token, @@ -207,7 +207,7 @@ RSpec.describe 'Contacts API', type: :request do end it 'enqueues a contact export job with sent_columns' do - expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email]).once + expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email], admin.email).once get "/api/v1/accounts/#{account.id}/contacts/export", headers: admin.create_new_auth_token, diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb index 3dcb47f71..9b16e48fb 100644 --- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb @@ -29,6 +29,7 @@ RSpec.describe 'Notifications API', type: :request do expect(response_json['data']['meta']['count']).to eq 2 # notification appear in descending order expect(response_json['data']['payload'].first['id']).to eq notification2.id + expect(response_json['data']['payload'].first['primary_actor']).not_to be_nil end end end @@ -178,6 +179,7 @@ RSpec.describe 'Notifications API', type: :request do expect(response).to have_http_status(:success) expect(notification.reload.snoozed_until).not_to eq('') + expect(notification.reload.meta['last_snoozed_at']).to be_nil end end end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 95fa2ae7f..bd604f650 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -213,6 +213,25 @@ RSpec.describe 'Accounts API', type: :request do end end + it 'updates onboarding step to invite_team if onboarding step is present in account custom attributes' do + account.update(custom_attributes: { onboarding_step: 'account_update' }) + put "/api/v1/accounts/#{account.id}", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team') + end + + it 'will not update onboarding step if onboarding step is not present in account custom attributes' do + put "/api/v1/accounts/#{account.id}", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to be_nil + end + it 'Throws error 422' do params[:name] = 'test' * 999 diff --git a/spec/controllers/api/v2/accounts_controller_spec.rb b/spec/controllers/api/v2/accounts_controller_spec.rb index 82693ed2b..182ebadac 100644 --- a/spec/controllers/api/v2/accounts_controller_spec.rb +++ b/spec/controllers/api/v2/accounts_controller_spec.rb @@ -30,6 +30,20 @@ RSpec.describe 'Accounts API', type: :request do end end + it 'updates the onboarding step in custom attributes' do + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do + allow(account_builder).to receive(:perform).and_return([user, account]) + + params = { email: email, user: nil, locale: nil, password: 'Password1!' } + + post api_v2_accounts_url, + params: params, + as: :json + + expect(account.reload.custom_attributes['onboarding_step']).to eq('profile_update') + end + end + it 'calls ChatwootCaptcha' do with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do captcha = double diff --git a/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb index 97e76a6cd..e5a8a5b7d 100644 --- a/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb @@ -41,5 +41,15 @@ RSpec.describe 'Agents API', type: :request do expect(response.body).to include('Account limit exceeded. Please purchase more licenses') end end + + context 'when onboarding step is present in account custom attributes' do + it 'removes onboarding step from account custom attributes' do + account.update(custom_attributes: { onboarding_step: 'completed' }) + + post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token + + expect(account.reload.custom_attributes).not_to include('onboarding_step') + end + end end end diff --git a/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb b/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb index 8ad75b42f..c74a2bcb5 100644 --- a/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb +++ b/spec/enterprise/controllers/enterprise/api/v2/accounts_controller_spec.rb @@ -51,6 +51,22 @@ RSpec.describe Enterprise::Api::V2::AccountsController, type: :request do end end + it 'updates the onboarding step in custom attributes' do + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do + allow(account_builder).to receive(:perform).and_return([user, account]) + + params = { email: email, user: nil, locale: nil, password: 'Password1!' } + + post api_v2_accounts_url, + params: params, + as: :json + + custom_attributes = account.custom_attributes + + expect(custom_attributes['onboarding_step']).to eq('profile_update') + end + end + it 'handles errors when fetching data from clearbit' do with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do allow(account_builder).to receive(:perform).and_return([user, account]) diff --git a/spec/jobs/account/contacts_export_job_spec.rb b/spec/jobs/account/contacts_export_job_spec.rb index e597ff637..b395967b4 100644 --- a/spec/jobs/account/contacts_export_job_spec.rb +++ b/spec/jobs/account/contacts_export_job_spec.rb @@ -24,17 +24,17 @@ RSpec.describe Account::ContactsExportJob do allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).with(account: account).and_return(mailer) allow(mailer).to receive(:contact_export_complete) - described_class.perform_now(account.id, []) + described_class.perform_now(account.id, [], 'test@test.com') file_url = Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export) expect(account.contacts_export).to be_present expect(file_url).to be_present - expect(mailer).to have_received(:contact_export_complete).with(file_url) + expect(mailer).to have_received(:contact_export_complete).with(file_url, 'test@test.com') end it 'generates valid data export file' do - described_class.perform_now(account.id, []) + described_class.perform_now(account.id, [], 'test@test.com') csv_data = CSV.parse(account.contacts_export.download, headers: true) emails = csv_data.pluck('email') diff --git a/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb index 024c3d0d6..94697dcd6 100644 --- a/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb +++ b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb @@ -14,10 +14,13 @@ RSpec.describe Notification::ReopenSnoozedNotificationsJob do it 'reopens snoozed notifications whose snooze until has passed' do described_class.perform_now + snoozed_until = snoozed_till_5_minutes_ago.reload.snoozed_until + expect(snoozed_till_5_minutes_ago.reload.snoozed_until).to be_nil expect(snoozed_till_tomorrow.reload.snoozed_until.to_date).to eq 1.day.from_now.to_date expect(snoozed_indefinitely.reload.snoozed_until).to be_nil expect(snoozed_indefinitely.reload.read_at).to be_nil + expect(snoozed_until).to eq(snoozed_till_5_minutes_ago.reload.meta['snoozed_until']) 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 f67b8300c..944475fb2 100644 --- a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb +++ b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb @@ -10,7 +10,6 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do 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) - Account::ContactsExportJob.perform_now(account.id, []) end describe 'slack_disconnect' do @@ -92,8 +91,8 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do end describe 'contact_export_complete' do - let!(:file_url) { Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export) } - let(:mail) { described_class.with(account: account).contact_export_complete(file_url).deliver_now } + 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.")