diff --git a/app/jobs/migration/conversations_first_reply_scheduler_job.rb b/app/jobs/migration/conversations_first_reply_scheduler_job.rb new file mode 100644 index 000000000..509357c1e --- /dev/null +++ b/app/jobs/migration/conversations_first_reply_scheduler_job.rb @@ -0,0 +1,17 @@ +# Delete migration and spec after 2 consecutive releases. +class Migration::ConversationsFirstReplySchedulerJob < ApplicationJob + queue_as :scheduled_jobs + + def perform(account) + account.conversations.each do |conversation| + # rubocop:disable Rails/SkipsModelValidations + if conversation.messages.outgoing.where("(additional_attributes->'campaign_id') is null").count.positive? + conversation.update_columns(first_reply_created_at: conversation.messages.outgoing.where("(additional_attributes->'campaign_id') is null") + .first.created_at) + else + conversation.update_columns(first_reply_created_at: nil) + end + # rubocop:enable Rails/SkipsModelValidations + end + end +end diff --git a/app/listeners/reporting_event_listener.rb b/app/listeners/reporting_event_listener.rb index d87b6574b..abd28ad32 100644 --- a/app/listeners/reporting_event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -36,6 +36,9 @@ class ReportingEventListener < BaseListener event_start_time: conversation.created_at, event_end_time: message.created_at ) + # rubocop:disable Rails/SkipsModelValidations + conversation.update_columns(first_reply_created_at: message.created_at) + # rubocop:enable Rails/SkipsModelValidations reporting_event.save end end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index ac02260a5..643f8fa56 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -2,27 +2,28 @@ # # Table name: conversations # -# id :integer not null, primary key -# additional_attributes :jsonb -# agent_last_seen_at :datetime -# assignee_last_seen_at :datetime -# contact_last_seen_at :datetime -# custom_attributes :jsonb -# identifier :string -# last_activity_at :datetime not null -# snoozed_until :datetime -# status :integer default("open"), not null -# uuid :uuid not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer not null -# assignee_id :integer -# campaign_id :bigint -# contact_id :bigint -# contact_inbox_id :bigint -# display_id :integer not null -# inbox_id :integer not null -# team_id :bigint +# id :integer not null, primary key +# additional_attributes :jsonb +# agent_last_seen_at :datetime +# assignee_last_seen_at :datetime +# contact_last_seen_at :datetime +# custom_attributes :jsonb +# first_reply_created_at :datetime +# identifier :string +# last_activity_at :datetime not null +# snoozed_until :datetime +# status :integer default("open"), not null +# uuid :uuid not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# assignee_id :integer +# campaign_id :bigint +# contact_id :bigint +# contact_inbox_id :bigint +# display_id :integer not null +# inbox_id :integer not null +# team_id :bigint # # Indexes # @@ -31,6 +32,7 @@ # index_conversations_on_assignee_id_and_account_id (assignee_id,account_id) # index_conversations_on_campaign_id (campaign_id) # index_conversations_on_contact_inbox_id (contact_inbox_id) +# index_conversations_on_first_reply_created_at (first_reply_created_at) # index_conversations_on_last_activity_at (last_activity_at) # index_conversations_on_status_and_account_id (status,account_id) # index_conversations_on_team_id (team_id) diff --git a/db/migrate/20220513145010_add_first_reply_activity_at_to_conversation.rb b/db/migrate/20220513145010_add_first_reply_activity_at_to_conversation.rb new file mode 100644 index 000000000..9afb58feb --- /dev/null +++ b/db/migrate/20220513145010_add_first_reply_activity_at_to_conversation.rb @@ -0,0 +1,23 @@ +class AddFirstReplyActivityAtToConversation < ActiveRecord::Migration[6.1] + def change + add_column :conversations, :first_reply_created_at, :datetime + add_index :conversations, :first_reply_created_at + + # rubocop:disable Rails/SkipsModelValidations + ::Conversation.update_all(first_reply_created_at: Time.now.utc) + # rubocop:enable Rails/SkipsModelValidations + + backfill_first_reply_activity_at_to_conversations + end + + private + + def backfill_first_reply_activity_at_to_conversations + ::Account.find_in_batches do |account_batch| + Rails.logger.info "Migrated till #{account_batch.first.id}\n" + account_batch.each do |account| + Migration::ConversationsFirstReplySchedulerJob.perform_later(account) + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 32f7dbefb..e7382572e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_05_11_072655) do +ActiveRecord::Schema.define(version: 2022_05_13_145010) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -385,11 +385,13 @@ ActiveRecord::Schema.define(version: 2022_05_11_072655) do t.datetime "snoozed_until" t.jsonb "custom_attributes", default: {} t.datetime "assignee_last_seen_at" + t.datetime "first_reply_created_at" t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true t.index ["account_id"], name: "index_conversations_on_account_id" t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id" t.index ["campaign_id"], name: "index_conversations_on_campaign_id" t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" + t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at" t.index ["last_activity_at"], name: "index_conversations_on_last_activity_at" t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id" t.index ["team_id"], name: "index_conversations_on_team_id" diff --git a/spec/jobs/migration/conversations_first_reply_scheduler_job_spec.rb b/spec/jobs/migration/conversations_first_reply_scheduler_job_spec.rb new file mode 100644 index 000000000..f16b51a3a --- /dev/null +++ b/spec/jobs/migration/conversations_first_reply_scheduler_job_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe Migration::ConversationsFirstReplySchedulerJob, type: :job do + subject(:job) { described_class.perform_later } + + let!(:account) { create(:account) } + let!(:inbox) { create(:inbox, account: account) } + let!(:user) { create(:user, account: account) } + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue('scheduled_jobs') + end + + context 'when there is an outgoing message in conversation' do + let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) } + let!(:message) do + create(:message, content: 'Hi', message_type: 'outgoing', account: account, inbox: inbox, + conversation: conversation) + end + + it 'updates the conversation first reply with the first outgoing message created time' do + create(:message, content: 'Hello', message_type: 'outgoing', account: account, inbox: inbox, + conversation: conversation) + + described_class.perform_now(account) + conversation.reload + + expect(conversation.messages.count).to eq 2 + expect(conversation.first_reply_created_at.to_i).to eq message.created_at.to_i + end + end + + context 'when there is no outgoing message in conversation' do + let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) } + + it 'updates the conversation first reply with nil' do + create(:message, content: 'Hello', message_type: 'incoming', account: account, inbox: inbox, + conversation: conversation) + + described_class.perform_now(account) + conversation.reload + + expect(conversation.messages.count).to eq 1 + expect(conversation.first_reply_created_at).to be_nil + end + end +end