diff --git a/app/jobs/migration/migrate_account_contacts_job.rb b/app/jobs/migration/migrate_account_contacts_job.rb index 61fdf6e6b..f491c95d7 100644 --- a/app/jobs/migration/migrate_account_contacts_job.rb +++ b/app/jobs/migration/migrate_account_contacts_job.rb @@ -1,4 +1,4 @@ -class Migration::MigrateAccountContactsJob < ApplicationJob +class Migration::PopulateAccountContactStatusAndSocialsJob < ApplicationJob queue_as :async_database_migration def perform(account_id) @@ -7,111 +7,47 @@ class Migration::MigrateAccountContactsJob < ApplicationJob Rails.logger.info "Processing contact migration for account #{account_id} (#{account.name})" - contacts = account.contacts.where(contact_type: 'visitor') + # Find the first lead contact to determine cutoff + first_lead = account.contacts.lead.order(:created_at).first + + # Build the contacts scope in a more readable way + contacts_scope = if first_lead.present? + account.contacts.visitor.where('created_at < ?', first_lead.created_at) + else + account.contacts.visitor + end + + total_count = contacts_scope.count processed_count = 0 - contacts.find_each do |contact| - processed_count += 1 if migrate_contact(contact) + contacts_scope.find_each(batch_size: 500) do |contact| + processed_count += 1 if sync_contact_attributes(contact) end - Rails.logger.info "Processed #{processed_count} contacts for account #{account_id}" + Rails.logger.info "Processed #{processed_count}/#{total_count} contacts for account #{account_id}" end private - def migrate_contact(contact) - return false unless contact.contact_type == 'visitor' + def sync_contact_attributes(contact) + # Track only the fields we care about + original_contact_type = contact.contact_type + original_additional = contact.additional_attributes.dup - # Migrate additional_attributes for social cases - migrate_social_attributes(contact) + # Run the sync service with migration + ::Contacts::SyncAttributes.new(contact).perform_with_migration - # Determine and update contact type - new_type = determine_contact_type(contact) - return false if new_type == 'visitor' + # Build update hash with only changed fields we care about + update_hash = {} + update_hash[:contact_type] = contact.contact_type if contact.contact_type != original_contact_type + update_hash[:additional_attributes] = contact.additional_attributes if contact.additional_attributes != original_additional - contact.update!(contact_type: new_type) + return false if update_hash.empty? + + # Use update_columns to bypass callbacks and avoid triggering events + # rubocop:disable Rails/SkipsModelValidations + contact.update_columns(update_hash) + # rubocop:enable Rails/SkipsModelValidations true - rescue StandardError => e - Rails.logger.error "Failed to migrate contact #{contact.id}: #{e.message}" - false - end - - def migrate_social_attributes(contact) - additional_attrs = contact.additional_attributes || {} - social_profiles = additional_attrs['social_profiles'] || {} - - return if social_profiles.blank? || social_profiles == {} - - modified = process_social_platforms(contact, additional_attrs, social_profiles) - return unless modified - - contact.update!(additional_attributes: additional_attrs) - end - - def process_social_platforms(contact, attrs, profiles) - modified = false - - # Telegram - migrate username only (ID comes from service) - modified ||= migrate_telegram_data(attrs, profiles) - - # Facebook - migrate username only - modified ||= migrate_facebook_data(attrs, profiles) - - # Instagram - migrate username only - modified ||= migrate_instagram_data(attrs, profiles) - - # LINE - special case: migrate ID and derive username from contact name - modified ||= migrate_line_data(contact, attrs, profiles) - - modified - end - - def migrate_telegram_data(attrs, profiles) - # Check both social_profiles.telegram and legacy username field - telegram_username = profiles['telegram'].presence || attrs['username'].presence - return false if telegram_username.blank? - - attrs['social_telegram_user_name'] ||= telegram_username - true - end - - def migrate_facebook_data(attrs, profiles) - return false if profiles['facebook'].blank? - - attrs['social_facebook_user_name'] ||= profiles['facebook'] - true - end - - def migrate_instagram_data(attrs, profiles) - return false if profiles['instagram'].blank? - - attrs['social_instagram_user_name'] ||= profiles['instagram'] - true - end - - def migrate_line_data(contact, attrs, profiles) - return false if profiles['line'].blank? - - attrs['social_line_user_id'] ||= profiles['line'] - attrs['social_line_user_name'] ||= contact.name.presence || profiles['line'] - true - end - - def identification?(contact) - contact.email.present? || contact.phone_number.present? || contact.identifier.present? || social_details_present?(contact) - end - - def social_details_present?(contact) - contact.additional_attributes.keys.any? do |key| - key.start_with?('social_') && contact.additional_attributes[key].present? - end - end - - def determine_contact_type(contact) - # Visitor: No identification info - return 'visitor' unless identification?(contact) - - # Lead: Has identification (email, phone, or social details) - 'lead' end end diff --git a/app/services/contacts/sync_attributes.rb b/app/services/contacts/sync_attributes.rb index bc10def66..ddd96e767 100644 --- a/app/services/contacts/sync_attributes.rb +++ b/app/services/contacts/sync_attributes.rb @@ -10,6 +10,12 @@ class Contacts::SyncAttributes set_contact_type end + def perform_with_migration + update_contact_location_and_country_code + migrate_social_attributes + set_contact_type + end + private def update_contact_location_and_country_code @@ -24,7 +30,7 @@ class Contacts::SyncAttributes return unless @contact.contact_type == 'visitor' # If the contact has an email or phone number or social details( facebook_user_id, instagram_user_id, etc) then it is a lead # If contact is from external channel like facebook, instagram, whatsapp, etc then it is a lead - return unless @contact.email.present? || @contact.phone_number.present? || social_details_present? + return unless @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present? || social_details_present? @contact.contact_type = 'lead' end @@ -34,4 +40,71 @@ class Contacts::SyncAttributes key.start_with?('social_') && @contact.additional_attributes[key].present? end end + + def migrate_social_attributes + return if @contact.additional_attributes.blank? + + attrs = @contact.additional_attributes + social_profiles = attrs['social_profiles'] || {} + + return if social_profiles.blank? + + # Migrate Telegram - check both social_profiles and legacy username field + migrate_telegram_attributes(attrs, social_profiles) + + # Migrate Facebook + migrate_facebook_attributes(attrs, social_profiles) + + # Migrate LINE + migrate_line_attributes(attrs, social_profiles) + end + + def migrate_telegram_attributes(attrs, profiles) + # Previous format + # { + # "social_profiles": { + # "telegram": "gilpadraocruz" + # } + # } + # New format + # { + # "social_telegram_user_name": "gilpadraocruz" + # } + telegram_username = profiles['telegram'].presence || attrs['username'].presence + return if telegram_username.blank? + + attrs['social_telegram_user_name'] ||= telegram_username + end + + def migrate_facebook_attributes(attrs, profiles) + # Previous format + # { + # "social_profiles": { + # "facebook": "gilpadraocruz" + # } + # } + # New format + # { + # "social_facebook_user_name": "gilpadraocruz" + # } + return if profiles['facebook'].blank? + + attrs['social_facebook_user_name'] ||= profiles['facebook'] + end + + def migrate_line_attributes(attrs, profiles) + # Previous format + # { + # "social_profiles": { + # "line": "gilpadraocruz" + # } + # } + # New format + # { + # "social_line_user_id": "gilpadraocruz" + # } + return if profiles['line'].blank? + + attrs['social_line_user_id'] ||= profiles['line'] + end end