diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb
index 8afd5b655..ba78cb805 100644
--- a/app/controllers/api/v1/accounts/contacts_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts_controller.rb
@@ -42,6 +42,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
     head :ok
   end
 
+  def export
+    column_names = params['column_names']
+    Account::ContactsExportJob.perform_later(Current.account.id, column_names)
+    head :ok, message: I18n.t('errors.contacts.export.success')
+  end
+
   # returns online contacts
   def active
     contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js
index 3599961e1..4def5ee2e 100644
--- a/app/javascript/dashboard/api/contacts.js
+++ b/app/javascript/dashboard/api/contacts.js
@@ -75,6 +75,10 @@ class ContactAPI extends ApiClient {
   destroyAvatar(contactId) {
     return axios.delete(`${this.url}/${contactId}/avatar`);
   }
+
+  exportContacts() {
+    return axios.get(`${this.url}/export`);
+  }
 }
 
 export default new ContactAPI();
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 042a1c1fc..feb6f0595 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -73,6 +73,13 @@
     "SUCCESS_MESSAGE": "Contacts saved successfully",
     "ERROR_MESSAGE": "There was an error, please try again"
   },
+  "EXPORT_CONTACTS": {
+    "BUTTON_LABEL": "Export",
+    "TITLE": "Export Contacts",
+    "DESC": "Export contacts to a CSV file.",
+    "SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to dowanlod.",
+    "ERROR_MESSAGE": "There was an error, please try again"
+  },
   "DELETE_NOTE": {
     "CONFIRM": {
       "TITLE": "Confirm Deletion",
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
index a7e2c3e29..bec3ce529 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
@@ -5,6 +5,7 @@
         :search-query="searchQuery"
         :segments-id="segmentsId"
         :on-search-submit="onSearchSubmit"
+        :on-export-submit="onExportSubmit"
         this-selected-contact-id=""
         :on-input-search="onInputSearch"
         :on-toggle-create="onToggleCreate"
@@ -92,6 +93,7 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
 import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
 import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews';
 import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
+import alertMixin from 'shared/mixins/alertMixin';
 import countries from 'shared/constants/countries.js';
 import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
 
@@ -110,6 +112,7 @@ export default {
     AddCustomViews,
     DeleteCustomViews,
   },
+  mixins: [alertMixin],
   props: {
     label: { type: String, default: '' },
     segmentsId: {
@@ -386,6 +389,16 @@ export default {
       this.$store.dispatch('contacts/clearContactFilters');
       this.fetchContacts(this.pageParameter);
     },
+    onExportSubmit() {
+      try {
+        this.$store.dispatch('contacts/export');
+        this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
+      } catch (error) {
+        this.showAlert(
+          error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
+        );
+      }
+    },
     setParamsForEditSegmentModal() {
       // Here we are setting the params for edit segment modal to show the existing values.
 
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue
index fd6dc8d52..46ab24a08 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue
@@ -87,6 +87,16 @@
         >
           {{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
         
+
+        
Hi
+ + +Your contact export file is ready to download.
+ ++Click here to download the export file. +
diff --git a/config/locales/en.yml b/config/locales/en.yml index daa5a295c..b4ea686d9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,6 +52,8 @@ en: contacts: import: failed: File is blank + export: + success: We will notify you once contacts export file is ready to view. email: invalid: Invalid email phone_number: diff --git a/config/routes.rb b/config/routes.rb index 73eccd3ee..9d862da2e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -115,6 +115,7 @@ Rails.application.routes.draw do get :search post :filter post :import + get :export end member do get :contactable_inboxes diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index f6141000e..cd5f977ca 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -168,6 +168,52 @@ RSpec.describe 'Contacts API', type: :request do end end + describe 'POST /api/v1/accounts/{account.id}/contacts/export' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/contacts/export" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user with out permission' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/contacts/export", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' 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 + + get "/api/v1/accounts/#{account.id}/contacts/export", + headers: admin.create_new_auth_token, + params: { column_names: nil } + + expect(response).to have_http_status(:success) + 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 + + get "/api/v1/accounts/#{account.id}/contacts/export", + headers: admin.create_new_auth_token, + params: { column_names: %w[phone_number email] } + + expect(response).to have_http_status(:success) + end + end + end + describe 'GET /api/v1/accounts/{account.id}/contacts/active' do context 'when it is an unauthenticated user' do it 'returns unauthorized' do diff --git a/spec/jobs/account/contacts_export_job_spec.rb b/spec/jobs/account/contacts_export_job_spec.rb new file mode 100644 index 000000000..d1c7dcff8 --- /dev/null +++ b/spec/jobs/account/contacts_export_job_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe Account::ContactsExportJob do + subject(:job) { described_class.perform_later } + + let!(:account) { create(:account) } + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue('low') + end + + context 'when export_contacts' do + before do + create(:contact, account: account, phone_number: '+910808080818', email: 'test1@text.example') + 8.times do + create(:contact, account: account) + end + create(:contact, account: account, phone_number: '+910808080808', email: 'test2@text.example') + end + + it 'generates CSV file and attach to account' do + mailer = double + 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, []) + + 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) + end + + it 'generates valid data export file' do + described_class.perform_now(account.id, []) + + csv_data = CSV.parse(account.contacts_export.download, headers: true) + first_row = csv_data[0] + last_row = csv_data[csv_data.length - 1] + first_contact = account.contacts.first + last_contact = account.contacts.last + + expect(csv_data.length).to eq(account.contacts.count) + expect([first_row['email'], last_row['email']]).to contain_exactly(first_contact.email, last_contact.email) + expect([first_row['phone_number'], last_row['phone_number']]).to contain_exactly(first_contact.phone_number, last_contact.phone_number) + 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 7190678cc..2bbb25b84 100644 --- a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb +++ b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb @@ -10,6 +10,7 @@ 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 @@ -55,4 +56,17 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do expect(mail.to).to eq([administrator.email]) end 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 } + + 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