chatwoot/app/services/labels/destroy_service.rb
Sojan Jose 9c1d1c4070
feat(labels): remove label associations asynchronously on delete (#13531)
## Summary
- Remove label deletion dependency on association cleanup by deleting
immediately and enqueueing a background job.
- Add `Labels::RemoveAssociationsJob` to strip deleted label references
from tagged conversations and contacts.
- Keep this version simple by removing the label count/prompt
requirement requested.

## Implementation notes
- Enqueue job from `Api::V1::Accounts::LabelsController#destroy` with
label title + account id.
- Background work performed in `Labels::DestroyService`.

## References
- Linear issue:
https://linear.app/chatwoot/issue/CW-4765/cw-2857-enhancement-removing-labels-is-inconsistent
- GitHub issue: https://github.com/chatwoot/chatwoot/issues/1249

## Testing
- `bundle exec rspec
spec/controllers/api/v1/accounts/labels_controller_spec.rb
spec/services/labels/destroy_service_spec.rb
spec/jobs/labels/remove_associations_job_spec.rb
spec/services/labels/update_service_spec.rb`
- `bundle exec rubocop
app/controllers/api/v1/accounts/labels_controller.rb
app/jobs/labels/remove_associations_job.rb
spec/controllers/api/v1/accounts/labels_controller_spec.rb
spec/jobs/labels/remove_associations_job_spec.rb
spec/services/labels/destroy_service_spec.rb`

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-08 13:40:36 +05:30

61 lines
1.8 KiB
Ruby

class Labels::DestroyService
pattr_initialize [:label_title!, :account_id!, :label_deleted_at!]
def perform
remove_conversation_labels
remove_contact_labels
end
private
def remove_conversation_labels
tagged_conversations.find_in_batches do |conversation_batch|
conversation_batch.each do |conversation|
update_conversation_cached_labels(conversation)
end
delete_label_taggings('Conversation', conversation_batch.map(&:id))
end
end
def remove_contact_labels
contact_label_taggings.in_batches do |tagging_batch|
ActsAsTaggableOn::Tagging.where(id: tagging_batch.select(:id)).delete_all
end
end
def update_conversation_cached_labels(conversation)
label_list = conversation.label_list.dup
label_list.remove(label_title)
# We only want the acts-as-taggable-on cache effect here, not Conversation callbacks/events.
# rubocop:disable Rails/SkipsModelValidations
conversation.update_column(:cached_label_list, label_list.join("#{ActsAsTaggableOn.delimiter} "))
# rubocop:enable Rails/SkipsModelValidations
end
def tagged_conversations
account.conversations.where(id: label_taggings_for('Conversation').select(:taggable_id))
end
def contact_label_taggings
label_taggings_for('Contact').where(taggable_id: account.contacts.select(:id))
end
def delete_label_taggings(taggable_type, taggable_ids)
ActsAsTaggableOn::Tagging
.where(id: label_taggings_for(taggable_type).where(taggable_id: taggable_ids).select(:id))
.delete_all
end
def label_taggings_for(taggable_type)
ActsAsTaggableOn::Tagging
.joins(:tag)
.where(context: 'labels', taggable_type: taggable_type, tags: { name: label_title })
.where('taggings.created_at <= ?', label_deleted_at)
end
def account
@account ||= Account.find(account_id)
end
end