Standardizes the contact company import/filter/automation contract on `company_name`. Closes #14096 Revives #9907 ## Why Contact company is read across the current CRM/contact UI from `additional_attributes['company_name']`, but CSV import and a few backend filter/automation paths still used the older `company` key. That meant imported company values could be saved in a place the dashboard, sorting, filters, and automation conditions did not consistently read from. Based on the production data check, the legacy `company` automation configuration is effectively dead: the affected account did not have contacts populated with `additional_attributes['company']`. So this PR intentionally avoids adding long-term fallback behavior and uses `company_name` as the single key going forward. ## What changed - Contact CSV import now writes only `company_name` into `additional_attributes['company_name']`. - The example contact import CSV now uses the `company_name` header. - Contact company sorting/filter config now uses `company_name`. - Automation condition config now uses `company_name`. - Existing standard automation conditions with `attribute_key: 'company'` are migrated to `company_name`. - Existing saved contact filters with standard `attribute_key: 'company'` are migrated to `company_name`. - Custom attributes named `company` are preserved and are not rewritten by the migration. ## How to test - Import a contact CSV with a `company_name` column and confirm the Contact Company field is populated. - Sort contacts by Company and confirm imported contacts are ordered correctly. - Create/edit an automation with Company as a condition and confirm it saves with `company_name`. - Verify existing saved contact filters and automation rules using the old standard `company` key are migrated to `company_name`. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
192 lines
8.6 KiB
Ruby
192 lines
8.6 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe DataImportJob do
|
|
subject(:job) { described_class.perform_later(data_import) }
|
|
|
|
let!(:data_import) { create(:data_import) }
|
|
|
|
describe 'enqueueing the job' do
|
|
it 'queues the job on the low priority queue' do
|
|
expect { job }.to have_enqueued_job(described_class)
|
|
.with(data_import)
|
|
.on_queue('low')
|
|
end
|
|
end
|
|
|
|
describe 'retrying the job' do
|
|
context 'when ActiveStorage::FileNotFoundError is raised' do
|
|
let(:import_file_double) { instance_double(ActiveStorage::Blob) }
|
|
|
|
before do
|
|
allow(data_import).to receive(:import_file).and_return(import_file_double)
|
|
allow(import_file_double).to receive(:open).and_raise(ActiveStorage::FileNotFoundError)
|
|
end
|
|
|
|
it 'retries the job' do
|
|
expect do
|
|
described_class.perform_now(data_import)
|
|
end.to have_enqueued_job(described_class).at_least(1).times
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'importing data' do
|
|
context 'when the data is valid' do
|
|
it 'imports data into the account' do
|
|
csv_length = CSV.parse(data_import.import_file.download, headers: true).length
|
|
|
|
described_class.perform_now(data_import)
|
|
expect(data_import.account.contacts.count).to eq(csv_length)
|
|
expect(data_import.reload.total_records).to eq(csv_length)
|
|
expect(data_import.reload.processed_records).to eq(csv_length)
|
|
contact = Contact.find_by(phone_number: '+918080808080')
|
|
expect(contact).to be_truthy
|
|
expect(contact['additional_attributes']['company_name']).to eq('My Company Name')
|
|
end
|
|
end
|
|
|
|
context 'when the data contains errors' do
|
|
it 'imports erroneous data into the account, skipping invalid records' do
|
|
# Last record is invalid because of duplicate email
|
|
invalid_data = [
|
|
%w[id first_name last_name email phone_number],
|
|
['1', 'Clarice', 'Uzzell', 'cuzzell0@mozilla.org', '+918484848484'],
|
|
['2', 'Marieann', 'Creegan', 'mcreegan1@cornell.edu', '+918484848485'],
|
|
['3', 'Nancey', 'Windibank', 'cuzzell0@mozilla.org', '+91848484848']
|
|
]
|
|
|
|
invalid_data_import = create(:data_import, import_file: generate_csv_file(invalid_data))
|
|
csv_data = CSV.parse(invalid_data_import.import_file.download, headers: true)
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(invalid_data_import)
|
|
expect(invalid_data_import.account.contacts.count).to eq(csv_length - 1)
|
|
expect(invalid_data_import.reload.total_records).to eq(csv_length)
|
|
expect(invalid_data_import.reload.processed_records).to eq(csv_length)
|
|
end
|
|
|
|
it 'will preserve emojis' do
|
|
data_import = create(:data_import,
|
|
import_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/data_import/with_emoji.csv'),
|
|
'text/csv'))
|
|
csv_data = CSV.parse(data_import.import_file.download, headers: true)
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(data_import)
|
|
expect(data_import.account.contacts.count).to eq(csv_length)
|
|
|
|
expect(data_import.account.contacts.first.name).to eq('T 🏠 🔥 Test')
|
|
end
|
|
|
|
it 'will not throw error for non utf-8 characters' do
|
|
invalid_data_import = create(:data_import,
|
|
import_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/data_import/invalid_bytes.csv'),
|
|
'text/csv'))
|
|
csv_data = CSV.parse(invalid_data_import.import_file.download, headers: true)
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(invalid_data_import)
|
|
expect(invalid_data_import.account.contacts.count).to eq(csv_length)
|
|
|
|
expect(invalid_data_import.account.contacts.first.name).to eq(csv_data[0]['name'].encode('UTF-8', 'binary', invalid: :replace,
|
|
undef: :replace, replace: ''))
|
|
end
|
|
|
|
it 'will strip UTF-8 BOM and import contacts correctly' do
|
|
bom_data_import = create(:data_import,
|
|
import_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/data_import/with_bom.csv'),
|
|
'text/csv'))
|
|
|
|
described_class.perform_now(bom_data_import)
|
|
expect(bom_data_import.account.contacts.count).to eq(1)
|
|
|
|
contact = bom_data_import.account.contacts.first
|
|
expect(contact.name).to eq('Ahmed')
|
|
expect(contact.email).to eq('ahmed@example.com')
|
|
expect(contact.phone_number).to eq('+971501234567')
|
|
end
|
|
end
|
|
|
|
context 'when the data contains existing records' do
|
|
let(:existing_data) do
|
|
[
|
|
%w[id name email phone_number company_name],
|
|
['1', 'Clarice Uzzell', 'cuzzell0@mozilla.org', '918080808080', 'Acmecorp'],
|
|
['2', 'Marieann Creegan', 'mcreegan1@cornell.edu', '+918080808081', 'Acmecorp'],
|
|
['3', 'Nancey Windibank', 'nwindibank2@bluehost.com', '+918080808082', 'Acmecorp']
|
|
]
|
|
end
|
|
let(:existing_data_import) { create(:data_import, import_file: generate_csv_file(existing_data)) }
|
|
let(:csv_data) { CSV.parse(existing_data_import.import_file.download, headers: true) }
|
|
|
|
context 'when the existing record has an email in import data' do
|
|
it 'updates the existing record with new data' do
|
|
contact = Contact.create!(email: csv_data[0]['email'], account_id: existing_data_import.account_id)
|
|
expect(contact.reload.phone_number).to be_nil
|
|
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(existing_data_import)
|
|
expect(existing_data_import.account.contacts.count).to eq(csv_length)
|
|
contact = Contact.from_email(csv_data[0]['email'])
|
|
expect(contact).to be_present
|
|
expect(contact.phone_number).to eq("+#{csv_data[0]['phone_number']}")
|
|
expect(contact.name).to eq((csv_data[0]['name']).to_s)
|
|
expect(contact.additional_attributes['company_name']).to eq((csv_data[0]['company_name']).to_s)
|
|
end
|
|
end
|
|
|
|
context 'when the existing record has a phone_number in import data' do
|
|
it 'updates the existing record with new data' do
|
|
contact = Contact.create!(account_id: existing_data_import.account_id, phone_number: csv_data[1]['phone_number'])
|
|
expect(contact.reload.email).to be_nil
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(existing_data_import)
|
|
expect(existing_data_import.account.contacts.count).to eq(csv_length)
|
|
|
|
contact = Contact.find_by(phone_number: "+#{csv_data[0]['phone_number']}")
|
|
expect(contact).to be_present
|
|
expect(contact.email).to eq(csv_data[0]['email'])
|
|
expect(contact.name).to eq((csv_data[0]['name']).to_s)
|
|
expect(contact.additional_attributes['company_name']).to eq((csv_data[0]['company_name']).to_s)
|
|
end
|
|
end
|
|
|
|
context 'when the existing record has both email and phone_number in import data' do
|
|
it 'skips importing the records' do
|
|
phone_contact = Contact.create!(account_id: existing_data_import.account_id, phone_number: csv_data[1]['phone_number'])
|
|
email_contact = Contact.create!(account_id: existing_data_import.account_id, email: csv_data[1]['email'])
|
|
|
|
csv_length = csv_data.length
|
|
|
|
described_class.perform_now(existing_data_import)
|
|
expect(phone_contact.reload.email).to be_nil
|
|
expect(email_contact.reload.phone_number).to be_nil
|
|
expect(existing_data_import.total_records).to eq(csv_length)
|
|
expect(existing_data_import.processed_records).to eq(csv_length - 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the CSV file is invalid' do
|
|
let(:invalid_csv_content) do
|
|
"id,name,email,phone_number,company_name\n" \
|
|
"1,\"Clarice Uzzell,\"missing_quote,918080808080,Acmecorp\n" \
|
|
'2,Marieann Creegan,,+918080808081,Acmecorp'
|
|
end
|
|
|
|
before do
|
|
import_file_double = instance_double(ActiveStorage::Blob)
|
|
allow(data_import).to receive(:import_file).and_return(import_file_double)
|
|
allow(import_file_double).to receive(:open).and_yield(StringIO.new(invalid_csv_content))
|
|
end
|
|
|
|
it 'does not import any data and handles the MalformedCSVError' do
|
|
expect { described_class.perform_now(data_import) }
|
|
.to change { data_import.reload.status }.from('pending').to('failed')
|
|
end
|
|
end
|
|
end
|
|
end
|