Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c11ecc56 | ||
|
|
4705a5c3ff |
@@ -16,6 +16,7 @@ const props = defineProps({
|
|||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' },
|
||||||
email: { type: String, default: '' },
|
email: { type: String, default: '' },
|
||||||
additionalAttributes: { type: Object, default: () => ({}) },
|
additionalAttributes: { type: Object, default: () => ({}) },
|
||||||
|
countryCode: { type: String, default: '' },
|
||||||
phoneNumber: { type: String, default: '' },
|
phoneNumber: { type: String, default: '' },
|
||||||
thumbnail: { type: String, default: '' },
|
thumbnail: { type: String, default: '' },
|
||||||
availabilityStatus: { type: String, default: null },
|
availabilityStatus: { type: String, default: null },
|
||||||
@@ -42,6 +43,7 @@ const getInitialContactData = () => ({
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
email: props.email,
|
email: props.email,
|
||||||
phoneNumber: props.phoneNumber,
|
phoneNumber: props.phoneNumber,
|
||||||
|
countryCode: props.countryCode,
|
||||||
additionalAttributes: props.additionalAttributes,
|
additionalAttributes: props.additionalAttributes,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,12 +60,13 @@ const countriesMap = computed(() => {
|
|||||||
|
|
||||||
const countryDetails = computed(() => {
|
const countryDetails = computed(() => {
|
||||||
const attributes = props.additionalAttributes || {};
|
const attributes = props.additionalAttributes || {};
|
||||||
const { country, countryCode, city } = attributes;
|
const { country, countryCode: additionalCountryCode, city } = attributes;
|
||||||
|
const countryCode = props.countryCode || additionalCountryCode;
|
||||||
|
|
||||||
if (!country && !countryCode) return null;
|
if (!country && !countryCode) return null;
|
||||||
|
|
||||||
const activeCountry =
|
const activeCountry =
|
||||||
countriesMap.value[country] || countriesMap.value[countryCode];
|
countriesMap.value[countryCode] || countriesMap.value[country];
|
||||||
|
|
||||||
if (!activeCountry) return null;
|
if (!activeCountry) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const prepareStateBasedOnProps = () => {
|
|||||||
name = '',
|
name = '',
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
|
countryCode: contactCountryCode = '',
|
||||||
additionalAttributes = {},
|
additionalAttributes = {},
|
||||||
} = props.contactData || {};
|
} = props.contactData || {};
|
||||||
const { firstName, lastName } = splitName(name || '');
|
const { firstName, lastName } = splitName(name || '');
|
||||||
@@ -122,7 +123,7 @@ const prepareStateBasedOnProps = () => {
|
|||||||
additionalAttributes: {
|
additionalAttributes: {
|
||||||
description,
|
description,
|
||||||
companyName,
|
companyName,
|
||||||
countryCode,
|
countryCode: contactCountryCode || countryCode,
|
||||||
country,
|
country,
|
||||||
city,
|
city,
|
||||||
socialProfiles: {
|
socialProfiles: {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const handleAvatarHover = (id, isHovered) => {
|
|||||||
:email="contact.email"
|
:email="contact.email"
|
||||||
:thumbnail="contact.thumbnail"
|
:thumbnail="contact.thumbnail"
|
||||||
:phone-number="contact.phoneNumber"
|
:phone-number="contact.phoneNumber"
|
||||||
|
:country-code="contact.countryCode"
|
||||||
:additional-attributes="contact.additionalAttributes"
|
:additional-attributes="contact.additionalAttributes"
|
||||||
:availability-status="contact.availabilityStatus"
|
:availability-status="contact.availabilityStatus"
|
||||||
:is-expanded="expandedCardId === contact.id"
|
:is-expanded="expandedCardId === contact.id"
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
countryCode: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
@@ -60,12 +64,17 @@ const updatedAtTime = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const countryDetails = computed(() => {
|
const countryDetails = computed(() => {
|
||||||
const { country, countryCode, city } = props.additionalAttributes;
|
const {
|
||||||
|
country,
|
||||||
|
countryCode: additionalCountryCode,
|
||||||
|
city,
|
||||||
|
} = props.additionalAttributes;
|
||||||
|
const countryCode = props.countryCode || additionalCountryCode;
|
||||||
|
|
||||||
if (!country && !countryCode) return null;
|
if (!country && !countryCode) return null;
|
||||||
|
|
||||||
const activeCountry =
|
const activeCountry =
|
||||||
countriesMap.value[country] || countriesMap.value[countryCode];
|
countriesMap.value[countryCode] || countriesMap.value[country];
|
||||||
|
|
||||||
if (!activeCountry) return null;
|
if (!activeCountry) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const accountId = useMapGetter('getCurrentAccountId');
|
|||||||
:name="contact.name"
|
:name="contact.name"
|
||||||
:email="contact.email"
|
:email="contact.email"
|
||||||
:phone="contact.phoneNumber"
|
:phone="contact.phoneNumber"
|
||||||
|
:country-code="contact.countryCode"
|
||||||
:additional-attributes="contact.additionalAttributes"
|
:additional-attributes="contact.additionalAttributes"
|
||||||
:account-id="accountId"
|
:account-id="accountId"
|
||||||
:thumbnail="contact.thumbnail"
|
:thumbnail="contact.thumbnail"
|
||||||
|
|||||||
@@ -72,12 +72,14 @@ export default {
|
|||||||
city = '',
|
city = '',
|
||||||
country_code: countryCode,
|
country_code: countryCode,
|
||||||
} = this.additionalAttributes;
|
} = this.additionalAttributes;
|
||||||
|
const canonicalCountryCode =
|
||||||
|
this.contact.country_code || this.contact.countryCode || countryCode;
|
||||||
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
||||||
|
|
||||||
if (!cityAndCountry) {
|
if (!cityAndCountry) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return this.findCountryFlag(countryCode, cityAndCountry);
|
return this.findCountryFlag(canonicalCountryCode, cityAndCountry);
|
||||||
},
|
},
|
||||||
socialProfiles() {
|
socialProfiles() {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Account::ContactsExportJob < ApplicationJob
|
|||||||
|
|
||||||
def value_for_header(contact, header)
|
def value_for_header(contact, header)
|
||||||
return contact_labels_by_id.fetch(contact.id, []).join(LABELS_DELIMITER) if header == LABELS_COLUMN
|
return contact_labels_by_id.fetch(contact.id, []).join(LABELS_DELIMITER) if header == LABELS_COLUMN
|
||||||
|
return contact.canonical_country_code if header == 'country_code'
|
||||||
|
|
||||||
contact.send(header)
|
contact.send(header)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module ContactCountryHelpers
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def canonical_country_code
|
||||||
|
attributes = additional_attributes || {}
|
||||||
|
|
||||||
|
CountryCodeNormalizer.normalize(self[:country_code]) ||
|
||||||
|
CountryCodeNormalizer.normalize(attributes['country_code']) ||
|
||||||
|
CountryCodeNormalizer.normalize(attributes['country'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def canonical_country_name
|
||||||
|
CountryCodeNormalizer.name_for(canonical_country_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
def additional_attributes_with_canonical_country
|
||||||
|
attributes = additional_attributes.deep_dup || {}
|
||||||
|
code = canonical_country_code
|
||||||
|
return attributes if code.blank?
|
||||||
|
|
||||||
|
attributes.merge(
|
||||||
|
'country_code' => code,
|
||||||
|
'country' => canonical_country_name || attributes['country']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
class Contact < ApplicationRecord
|
class Contact < ApplicationRecord
|
||||||
include Avatarable
|
include Avatarable
|
||||||
include AvailabilityStatusable
|
include AvailabilityStatusable
|
||||||
|
include ContactCountryHelpers
|
||||||
include Labelable
|
include Labelable
|
||||||
include LlmFormattable
|
include LlmFormattable
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
require 'tzinfo'
|
||||||
|
|
||||||
|
class CountryCodeNormalizer
|
||||||
|
COUNTRY_ALIASES = {
|
||||||
|
'united states of america' => 'US',
|
||||||
|
'usa' => 'US',
|
||||||
|
'uk' => 'GB',
|
||||||
|
'united kingdom' => 'GB'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def normalize(value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
value = value.to_s.strip
|
||||||
|
code = value.upcase
|
||||||
|
return code if country_by_code.key?(code)
|
||||||
|
|
||||||
|
country_by_name[normalized_name(value)] || COUNTRY_ALIASES[normalized_name(value)]
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_for(code)
|
||||||
|
country_by_code[normalize(code)]&.name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def country_by_code
|
||||||
|
@country_by_code ||= TZInfo::Country.all_codes.index_with { |code| TZInfo::Country.get(code) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def country_by_name
|
||||||
|
@country_by_name ||= country_by_code.each_with_object({}) do |(code, country), result|
|
||||||
|
result[normalized_name(country.name)] = code
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalized_name(value)
|
||||||
|
value.to_s.strip.downcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -26,7 +26,7 @@ class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter
|
|||||||
attributes << "Email: #{@record.email}"
|
attributes << "Email: #{@record.email}"
|
||||||
attributes << "Phone: #{@record.phone_number}"
|
attributes << "Phone: #{@record.phone_number}"
|
||||||
attributes << "Location: #{@record.location}"
|
attributes << "Location: #{@record.location}"
|
||||||
attributes << "Country Code: #{@record.country_code}"
|
attributes << "Country Code: #{@record.canonical_country_code}"
|
||||||
@record.account.custom_attribute_definitions.with_attribute_model('contact_attribute').each do |attribute|
|
@record.account.custom_attribute_definitions.with_attribute_model('contact_attribute').each do |attribute|
|
||||||
attributes << "#{attribute.attribute_display_name}: #{@record.custom_attributes[attribute.attribute_key]}"
|
attributes << "#{attribute.attribute_display_name}: #{@record.custom_attributes[attribute.attribute_key]}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ json.id contact.id
|
|||||||
json.name contact.name
|
json.name contact.name
|
||||||
json.phone_number contact.phone_number
|
json.phone_number contact.phone_number
|
||||||
json.identifier contact.identifier
|
json.identifier contact.identifier
|
||||||
json.additional_attributes contact.additional_attributes
|
json.country_code contact.canonical_country_code
|
||||||
|
json.additional_attributes contact.additional_attributes_with_canonical_country
|
||||||
json.last_activity_at contact.last_activity_at&.to_i
|
json.last_activity_at contact.last_activity_at&.to_i
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
json.additional_attributes resource.additional_attributes
|
json.additional_attributes resource.additional_attributes_with_canonical_country
|
||||||
json.availability_status resource.availability_status
|
json.availability_status resource.availability_status
|
||||||
|
json.country_code resource.canonical_country_code
|
||||||
json.email resource.email
|
json.email resource.email
|
||||||
json.id resource.id
|
json.id resource.id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ RSpec.describe 'Contacts API', type: :request do
|
|||||||
expect(contact_inboxes_source_ids).to include(contact_inbox.source_id)
|
expect(contact_inboxes_source_ids).to include(contact_inbox.source_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns canonical country attributes for contact display' do
|
||||||
|
contact.update!(additional_attributes: { country: 'United States' })
|
||||||
|
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
response_body = response.parsed_body
|
||||||
|
response_contact = response_body['payload'].find { |item| item['id'] == contact.id }
|
||||||
|
|
||||||
|
expect(response_contact['additional_attributes']).to include(
|
||||||
|
'country_code' => 'US',
|
||||||
|
'country' => 'United States'
|
||||||
|
)
|
||||||
|
expect(response_contact['country_code']).to eq('US')
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns all contacts without contact inboxes' do
|
it 'returns all contacts without contact inboxes' do
|
||||||
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false",
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false",
|
||||||
headers: admin.create_new_auth_token,
|
headers: admin.create_new_auth_token,
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ RSpec.describe Account::ContactsExportJob do
|
|||||||
expect(row['labels']).to eq('vip')
|
expect(row['labels']).to eq('vip')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'exports canonical country codes from legacy country data' do
|
||||||
|
legacy_country_contact = create(:contact, account: account, email: 'legacy-country@example.com',
|
||||||
|
additional_attributes: { country: 'United States' })
|
||||||
|
|
||||||
|
described_class.perform_now(account.id, user.id, %w[email country_code], {})
|
||||||
|
|
||||||
|
csv_content = account.contacts_export.download.force_encoding('UTF-8').delete_prefix("\xEF\xBB\xBF")
|
||||||
|
csv_data = CSV.parse(csv_content, headers: true)
|
||||||
|
row = csv_data.find { |record| record['email'] == legacy_country_contact.email }
|
||||||
|
|
||||||
|
expect(row['country_code']).to eq('US')
|
||||||
|
end
|
||||||
|
|
||||||
it 'bulk loads labels while exporting contacts' do
|
it 'bulk loads labels while exporting contacts' do
|
||||||
create(:label, account: account, title: 'vip')
|
create(:label, account: account, title: 'vip')
|
||||||
create(:label, account: account, title: 'support')
|
create(:label, account: account, title: 'support')
|
||||||
|
|||||||
@@ -93,6 +93,42 @@ RSpec.describe Contact do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#canonical_country_code' do
|
||||||
|
it 'prefers the contact country_code column when it has a valid country code' do
|
||||||
|
contact = create(:contact, additional_attributes: { country_code: 'IN', country: 'India' })
|
||||||
|
contact[:country_code] = 'US'
|
||||||
|
|
||||||
|
expect(contact.canonical_country_code).to eq('US')
|
||||||
|
expect(contact.canonical_country_name).to eq('United States')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'falls back to additional_attributes country_code' do
|
||||||
|
contact = create(:contact, additional_attributes: { country_code: 'in' })
|
||||||
|
|
||||||
|
expect(contact.canonical_country_code).to eq('IN')
|
||||||
|
expect(contact.canonical_country_name).to eq('India')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'normalizes legacy country names from additional_attributes country' do
|
||||||
|
contact = create(:contact, additional_attributes: { country: 'United States' })
|
||||||
|
|
||||||
|
expect(contact.canonical_country_code).to eq('US')
|
||||||
|
expect(contact.canonical_country_name).to eq('United States')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#additional_attributes_with_canonical_country' do
|
||||||
|
it 'returns additional attributes with normalized country values without persisting them' do
|
||||||
|
contact = create(:contact, additional_attributes: { country: 'United States' })
|
||||||
|
|
||||||
|
expect(contact.additional_attributes_with_canonical_country).to include(
|
||||||
|
'country_code' => 'US',
|
||||||
|
'country' => 'United States'
|
||||||
|
)
|
||||||
|
expect(contact.reload.additional_attributes).not_to include('country_code')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a contact is created' do
|
context 'when a contact is created' do
|
||||||
it 'has contact type "visitor" by default' do
|
it 'has contact type "visitor" by default' do
|
||||||
contact = create(:contact)
|
contact = create(:contact)
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ RSpec.describe LlmFormatter::ContactLlmFormatter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when contact has legacy country data' do
|
||||||
|
before do
|
||||||
|
contact.update!(additional_attributes: { country: 'United States' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses the canonical country code' do
|
||||||
|
expect(formatter.format).to include('Country Code: US')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when contact has custom attributes' do
|
context 'when contact has custom attributes' do
|
||||||
let!(:custom_attribute) do
|
let!(:custom_attribute) do
|
||||||
create(:custom_attribute_definition, account: account, attribute_model: 'contact_attribute', attribute_display_name: 'Company')
|
create(:custom_attribute_definition, account: account, attribute_model: 'contact_attribute', attribute_display_name: 'Company')
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ properties:
|
|||||||
availability_status:
|
availability_status:
|
||||||
type: string
|
type: string
|
||||||
description: The availability status of the contact
|
description: The availability status of the contact
|
||||||
|
country_code:
|
||||||
|
type: string
|
||||||
|
description: The normalized ISO country code of the contact
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
description: The email address of the contact
|
description: The email address of the contact
|
||||||
|
|||||||
Reference in New Issue
Block a user