## Description Adds an admin-only Intercom import workflow under Settings > Data. Admins can connect an Intercom access token, start named historical contact/conversation imports, monitor active and previous import runs, review paginated skip/error logs, download skip logs, and route imported conversations into source-bucket API inboxes that can be renamed later. The import path stores durable source mappings, batches Intercom contact/conversation pages through Sidekiq, records already-imported records as skipped, and writes historical messages without normal outbound delivery callbacks. The PR also includes the Intercom import PRD/TDD document for review context. Closes [CW-7519](https://linear.app/chatwoot/issue/CW-7519/explore-intercom-import) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [x] This change requires a documentation update ## How Has This Been Tested? Tested importing using actual data through integration. Screenshots: <img width="1800" height="948" alt="Screenshot 2026-07-02 at 10 48 48 PM" src="https://github.com/user-attachments/assets/e74d9ed6-0bca-47de-b6ef-e589afcddfde" /> <img width="1800" height="1008" alt="Screenshot 2026-07-02 at 10 49 03 PM" src="https://github.com/user-attachments/assets/1bd12fdb-0a47-4287-ac1d-ea308e70a9cd" /> <img width="1800" height="1005" alt="Screenshot 2026-07-02 at 10 49 21 PM" src="https://github.com/user-attachments/assets/3d8145f5-1794-4cc3-b3fa-de5cd80e6ca3" /> <img width="1800" height="1002" alt="Screenshot 2026-07-02 at 10 49 38 PM" src="https://github.com/user-attachments/assets/6f818efd-4193-43c2-84eb-66970dca4490" /> Passed locally: ```sh eval "$(rbenv init -)" && bundle exec rspec spec/models/data_import_spec.rb spec/jobs/data_import_job_spec.rb spec/requests/api/v1/accounts/data_imports_spec.rb spec/requests/api/v1/accounts/integrations/intercom_spec.rb spec/jobs/data_imports/intercom/import_jobs_spec.rb spec/services/data_imports/intercom/importer_spec.rb spec/services/data_imports/intercom/placeholder_inbox_builder_spec.rb spec/services/data_imports/intercom/source_bucket_spec.rb ``` ```sh eval "$(rbenv init -)" && bundle exec rubocop app/controllers/api/v1/accounts/data_imports_controller.rb app/controllers/api/v1/accounts/integrations/intercom_controller.rb app/jobs/data_imports/intercom app/models/data_import.rb app/models/data_import_error.rb app/models/data_import_item.rb app/models/data_import_mapping.rb app/models/integrations/hook.rb app/policies/data_import_policy.rb app/policies/hook_policy.rb app/services/data_imports/intercom db/migrate/20260702000000_expand_data_imports_for_intercom_imports.rb db/migrate/20260702000001_create_data_import_items.rb db/migrate/20260702000002_create_data_import_mappings.rb db/migrate/20260702000003_create_data_import_errors.rb spec/jobs/data_imports/intercom spec/requests/api/v1/accounts/data_imports_spec.rb spec/requests/api/v1/accounts/integrations/intercom_spec.rb spec/services/data_imports/intercom ``` ```sh pnpm exec eslint app/javascript/dashboard/api/dataImports.js app/javascript/dashboard/api/integrations.js app/javascript/dashboard/routes/dashboard/settings/data/Index.vue app/javascript/dashboard/routes/dashboard/settings/data/Show.vue app/javascript/dashboard/routes/dashboard/settings/data/data.routes.js app/javascript/dashboard/routes/dashboard/settings/data/importStatus.js app/javascript/dashboard/routes/dashboard/settings/integrations/Intercom.vue app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js app/javascript/dashboard/routes/dashboard/settings/settings.routes.js app/javascript/dashboard/components-next/sidebar/Sidebar.vue app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue ``` ```sh git diff --check ``` Note: the RSpec boot logs the existing local `chatwoot_dev` purge warning because other database sessions are open, then continues and completes with 52 examples, 0 failures. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
118 lines
3.7 KiB
Ruby
118 lines
3.7 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: data_imports
|
|
#
|
|
# id :bigint not null, primary key
|
|
# abandoned_at :datetime
|
|
# access_token :text
|
|
# completed_at :datetime
|
|
# cursor :jsonb not null
|
|
# data_type :string not null
|
|
# import_types :jsonb not null
|
|
# last_error_at :datetime
|
|
# name :string
|
|
# processed_records :integer
|
|
# processing_errors :text
|
|
# source_metadata :jsonb not null
|
|
# source_provider :string
|
|
# source_type :string
|
|
# started_at :datetime
|
|
# stats :jsonb not null
|
|
# status :integer default("pending"), not null
|
|
# total_records :integer
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :bigint not null
|
|
# initiated_by_id :integer
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_data_imports_on_account_id (account_id)
|
|
# index_data_imports_on_initiated_by_id (initiated_by_id)
|
|
# index_data_imports_on_source_provider (source_provider)
|
|
#
|
|
class DataImport < ApplicationRecord
|
|
ACTIVE_INTERCOM_IMPORT_RUN_ID_KEY = 'active_intercom_import_run_id'.freeze
|
|
LEGACY_DATA_TYPES = ['contacts'].freeze
|
|
INTEGRATION_DATA_TYPES = ['intercom'].freeze
|
|
IMPORT_TYPES = %w[contacts conversations].freeze
|
|
|
|
belongs_to :account
|
|
belongs_to :initiated_by, class_name: 'User', optional: true
|
|
|
|
encrypts :access_token if Chatwoot.encryption_configured?
|
|
|
|
has_many :items, class_name: 'DataImportItem', dependent: :destroy_async
|
|
has_many :mappings, class_name: 'DataImportMapping', dependent: :destroy_async
|
|
has_many :import_errors, class_name: 'DataImportError', dependent: :destroy_async
|
|
|
|
validates :data_type, inclusion: { in: LEGACY_DATA_TYPES + INTEGRATION_DATA_TYPES, message: I18n.t('errors.data_import.data_type.invalid') }
|
|
validates :access_token, presence: true, on: :create, if: :intercom_import?
|
|
validate :validate_import_types
|
|
|
|
enum status: { pending: 0, processing: 1, completed: 2, failed: 3, completed_with_errors: 6, abandoned: 7 }
|
|
|
|
scope :active_intercom, -> { where(data_type: 'intercom', source_provider: 'intercom', status: [:pending, :processing]) }
|
|
|
|
has_one_attached :import_file
|
|
has_one_attached :failed_records
|
|
|
|
after_create_commit :process_data_import
|
|
|
|
def legacy_contacts_csv_import?
|
|
data_type == 'contacts' && source_provider.blank?
|
|
end
|
|
|
|
def intercom_import?
|
|
data_type == 'intercom' && source_provider == 'intercom'
|
|
end
|
|
|
|
def restartable?
|
|
failed? || abandoned?
|
|
end
|
|
|
|
def abandonable?
|
|
intercom_import? && (pending? || processing?)
|
|
end
|
|
|
|
def abandon!
|
|
self.class.transaction do
|
|
abandonable_import = self.class.lock.find_by(
|
|
id: id,
|
|
data_type: 'intercom',
|
|
source_provider: 'intercom',
|
|
status: [:pending, :processing]
|
|
)
|
|
abandonable_import&.update!(status: :abandoned, abandoned_at: Time.current)
|
|
end
|
|
reload
|
|
end
|
|
|
|
def active_intercom_import_run_id
|
|
source_metadata.to_h[ACTIVE_INTERCOM_IMPORT_RUN_ID_KEY]
|
|
end
|
|
|
|
def assign_active_intercom_import_run_id
|
|
self.source_metadata = source_metadata.to_h.merge(ACTIVE_INTERCOM_IMPORT_RUN_ID_KEY => SecureRandom.uuid)
|
|
active_intercom_import_run_id
|
|
end
|
|
|
|
private
|
|
|
|
def process_data_import
|
|
return unless legacy_contacts_csv_import?
|
|
|
|
# we wait for the file to be uploaded to the cloud
|
|
DataImportJob.set(wait: 1.minute).perform_later(self)
|
|
end
|
|
|
|
def validate_import_types
|
|
return if import_types.blank?
|
|
|
|
invalid_types = import_types - IMPORT_TYPES
|
|
return if invalid_types.blank?
|
|
|
|
errors.add(:import_types, "contains unsupported values: #{invalid_types.join(', ')}")
|
|
end
|
|
end
|