Files
chatwoot/app/services/data_imports/intercom/activity_content_builder.rb
11c65f3b9a feat: Intercom import workflow (#14922)
## 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>
2026-07-13 15:25:07 +05:30

85 lines
2.4 KiB
Ruby

class DataImports::Intercom::ActivityContentBuilder
EVENT_KEYS = {
'assignment' => :assignment,
'assign_and_reopen' => :assign_and_reopen,
'open' => :open,
'close' => :close,
'snoozed' => :snoozed,
'participant_added' => :participant_added,
'participant_removed' => :participant_removed,
'conversation_attribute_updated_by_admin' => :conversation_attribute_updated,
'conversation_attribute_updated_by_user' => :conversation_attribute_updated,
'conversation_attribute_updated_by_workflow' => :conversation_attribute_updated,
'ticket_attribute_updated_by_admin' => :ticket_attribute_updated,
'ticket_state_updated_by_admin' => :ticket_state_updated,
'custom_action_started' => :custom_action_started,
'custom_action_finished' => :custom_action_finished,
'quick_reply' => :quick_reply
}.freeze
def initialize(part)
@part = part.to_h
end
def perform
append_body(translated_content)
end
private
def translated_content
key = EVENT_KEYS.fetch(event_type, :generic)
key = "#{key}_with_target" if target_aware_event?(key) && target_name.present?
I18n.t(
"data_imports.intercom.activities.#{key}",
actor: actor_name,
target: target_name,
event: event_type.tr('_', ' ')
)
end
def event_type
@part['part_type'].to_s
end
def actor_name
author = @part['author'].to_h
return author['name'] if author['name'].present?
case author['type']
when 'user', 'contact', 'lead'
'Contact'
when 'bot'
'Intercom automation'
else
automation_event? ? 'Intercom automation' : 'Intercom teammate'
end
end
def target_name
assigned_to = @part['assigned_to'].to_h
event_details = @part['event_details'].to_h
participant = event_details['participant'].to_h
assigned_to['name'].presence || participant['name'].presence || event_details['participant_name'].presence || event_details['name'].presence
end
def target_aware_event?(key)
%i[assignment assign_and_reopen participant_added participant_removed].include?(key)
end
def automation_event?
event_type.include?('workflow') || event_type.start_with?('custom_action')
end
def append_body(content)
fragment = Nokogiri::HTML5.fragment(@part['body'].to_s)
fragment.css('script, style').remove
body = fragment.text.squish
return content if body.blank? || content.downcase.include?(body.downcase)
"#{content}: #{body}"
end
end