Merge branch 'develop' into fix/CW-7007

This commit is contained in:
Sivin Varghese
2026-05-08 14:15:51 +05:30
committed by GitHub
6 changed files with 239 additions and 0 deletions
@@ -18,7 +18,16 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
end
def destroy
label_title = @label.title
account_id = Current.account.id
label_deleted_at = Time.current
@label.destroy!
Labels::RemoveAssociationsJob.perform_later(
label_title: label_title,
account_id: account_id,
label_deleted_at: label_deleted_at
)
head :ok
end
@@ -0,0 +1,11 @@
class Labels::RemoveAssociationsJob < ApplicationJob
queue_as :default
def perform(label_title:, account_id:, label_deleted_at:)
Labels::DestroyService.new(
label_title: label_title,
account_id: account_id,
label_deleted_at: label_deleted_at
).perform
end
end
+60
View File
@@ -0,0 +1,60 @@
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
@@ -3,6 +3,7 @@ require 'rails_helper'
RSpec.describe 'Label API', type: :request do
let!(:account) { create(:account) }
let!(:label) { create(:label, account: account) }
let!(:conversation) { create(:conversation, account: account) }
describe 'GET /api/v1/accounts/{account.id}/labels' do
context 'when it is an unauthenticated user' do
@@ -101,4 +102,39 @@ RSpec.describe 'Label API', type: :request do
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/labels/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/labels/#{label.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'deletes the label and enqueues label cleanup' do
label_deleted_at = Time.zone.parse('2026-05-07 10:00:00 UTC')
conversation.label_list.add(label.title)
conversation.save!
clear_enqueued_jobs
travel_to(label_deleted_at) do
expect do
delete "/api/v1/accounts/#{account.id}/labels/#{label.id}", headers: admin.create_new_auth_token, as: :json
end.to have_enqueued_job(Labels::RemoveAssociationsJob).with(
label_title: label.title,
account_id: account.id,
label_deleted_at: label_deleted_at
)
end
expect(response).to have_http_status(:ok)
expect(Label.exists?(label.id)).to be(false)
end
end
end
end
@@ -0,0 +1,21 @@
require 'rails_helper'
RSpec.describe Labels::RemoveAssociationsJob do
subject(:job) do
described_class.perform_later(
label_title: label_title,
account_id: account_id,
label_deleted_at: label_deleted_at
)
end
let(:label_title) { 'billing' }
let(:account_id) { 1 }
let(:label_deleted_at) { Time.current }
it 'queues the job' do
expect { job }.to have_enqueued_job(described_class)
.with(label_title: label_title, account_id: account_id, label_deleted_at: label_deleted_at)
.on_queue('default')
end
end
@@ -0,0 +1,102 @@
require 'rails_helper'
describe Labels::DestroyService do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
let(:label) { create(:label, account: account) }
let(:contact) { conversation.contact }
let(:label_deleted_at) { Time.zone.parse('2026-05-07 10:00:00 UTC') }
before do
conversation.label_list.add(label.title)
conversation.label_list.add('billing')
conversation.save!
contact.label_list.add(label.title)
contact.label_list.add('vip')
contact.save!
set_label_tagging_created_at(conversation, label_deleted_at - 1.minute)
set_label_tagging_created_at(contact, label_deleted_at - 1.minute)
end
describe '#perform' do
it 'removes label from associated conversations and contacts' do
described_class.new(
label_title: label.title,
account_id: account.id,
label_deleted_at: label_deleted_at
).perform
expect(conversation.reload.label_list).to eq(['billing'])
expect(conversation.cached_label_list).to eq('billing')
expect(contact.reload.label_list).to eq(['vip'])
end
it 'removes label associations after the label record is destroyed' do
label_title = label.title
label.destroy!
described_class.new(
label_title: label_title,
account_id: account.id,
label_deleted_at: label_deleted_at
).perform
expect(conversation.reload.label_list).to eq(['billing'])
expect(conversation.cached_label_list).to eq('billing')
expect(contact.reload.label_list).to eq(['vip'])
end
it 'does not remove labels from other accounts' do
other_account = create(:account)
other_conversation = create(:conversation, account: other_account)
other_conversation.label_list.add(label.title)
other_conversation.save!
set_label_tagging_created_at(other_conversation, label_deleted_at - 1.minute)
described_class.new(
label_title: label.title,
account_id: account.id,
label_deleted_at: label_deleted_at
).perform
expect(other_conversation.reload.label_list).to eq([label.title])
end
it 'does not dispatch conversation or contact update events' do
expect(Rails.configuration.dispatcher).not_to receive(:dispatch)
described_class.new(
label_title: label.title,
account_id: account.id,
label_deleted_at: label_deleted_at
).perform
end
it 'does not remove label associations created after the label was deleted' do
other_conversation = create(:conversation, account: account)
other_conversation.label_list.add(label.title)
other_conversation.save!
set_label_tagging_created_at(other_conversation, label_deleted_at + 1.minute)
described_class.new(
label_title: label.title,
account_id: account.id,
label_deleted_at: label_deleted_at
).perform
expect(conversation.reload.label_list).to eq(['billing'])
expect(conversation.cached_label_list).to eq('billing')
expect(contact.reload.label_list).to eq(['vip'])
expect(other_conversation.reload.label_list).to eq([label.title])
end
end
def set_label_tagging_created_at(record, created_at)
ActsAsTaggableOn::Tagging
.joins(:tag)
.find_by!(context: 'labels', taggable: record, tags: { name: label.title })
.update!(created_at: created_at)
end
end