fix(inbox): remove captain dependencies before delete
This commit is contained in:
parent
9b8ef5a828
commit
f6a96f9d4e
@ -2,6 +2,30 @@ class DeleteObjectJob < ApplicationJob
|
|||||||
queue_as :low
|
queue_as :low
|
||||||
|
|
||||||
BATCH_SIZE = 5_000
|
BATCH_SIZE = 5_000
|
||||||
|
INBOX_DEPENDENT_TABLES = %i[
|
||||||
|
captain_feedback_logs
|
||||||
|
captain_lifecycle_deliveries
|
||||||
|
captain_reminders
|
||||||
|
captain_reservations
|
||||||
|
captain_gallery_items
|
||||||
|
captain_inbox_automations
|
||||||
|
captain_inbox_reminder_settings
|
||||||
|
captain_inboxes
|
||||||
|
captain_notification_templates
|
||||||
|
captain_pricing_inboxes
|
||||||
|
captain_tool_configs
|
||||||
|
captain_unit_inboxes
|
||||||
|
jasmine_inbox_collections
|
||||||
|
jasmine_inbox_settings
|
||||||
|
jasmine_tool_configs
|
||||||
|
].freeze
|
||||||
|
INBOX_NULLIFY_TARGETS = [
|
||||||
|
[:captain_conversation_insights, :inbox_id],
|
||||||
|
[:captain_pricings, :inbox_id],
|
||||||
|
[:jasmine_collections, :owner_inbox_id],
|
||||||
|
[:captain_units, :inbox_id],
|
||||||
|
[:captain_units, :concierge_inbox_id]
|
||||||
|
].freeze
|
||||||
|
|
||||||
def perform(object, user = nil, ip = nil)
|
def perform(object, user = nil, ip = nil)
|
||||||
# Pre-purge heavy associations for large objects to avoid
|
# Pre-purge heavy associations for large objects to avoid
|
||||||
@ -23,6 +47,8 @@ class DeleteObjectJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def purge_heavy_associations(object)
|
def purge_heavy_associations(object)
|
||||||
|
purge_inbox_blocking_associations(object) if object.is_a?(Inbox)
|
||||||
|
|
||||||
klass = heavy_associations.keys.find { |k| object.is_a?(k) }
|
klass = heavy_associations.keys.find { |k| object.is_a?(k) }
|
||||||
return unless klass
|
return unless klass
|
||||||
|
|
||||||
@ -38,6 +64,71 @@ class DeleteObjectJob < ApplicationJob
|
|||||||
batch.each(&:destroy!)
|
batch.each(&:destroy!)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def purge_inbox_blocking_associations(inbox)
|
||||||
|
inbox_id = inbox.id
|
||||||
|
reservation_ids = select_ids(:captain_reservations, :inbox_id, inbox_id)
|
||||||
|
|
||||||
|
purge_reservation_children(reservation_ids)
|
||||||
|
|
||||||
|
# fazer.ai/Captain tables hold hard FKs to inboxes. If these survive, the
|
||||||
|
# async delete job fails and the UI shows the inbox again on refresh.
|
||||||
|
INBOX_DEPENDENT_TABLES.each { |table| delete_by_column(table, :inbox_id, inbox_id) }
|
||||||
|
nullify_inbox_references(inbox_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def purge_reservation_children(reservation_ids)
|
||||||
|
delete_where_in(:captain_lifecycle_deliveries, :captain_reservation_id, reservation_ids)
|
||||||
|
delete_where_in(:captain_pix_charges, :reservation_id, reservation_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def nullify_inbox_references(inbox_id)
|
||||||
|
INBOX_NULLIFY_TARGETS.each do |table, column|
|
||||||
|
nullify_by_column(table, column, inbox_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_ids(table, column, value)
|
||||||
|
return [] unless column_available?(table, column)
|
||||||
|
|
||||||
|
sql = "SELECT id FROM #{quote_table(table)} WHERE #{quote_column(column)} = #{Integer(value)}"
|
||||||
|
db.select_values(sql).map(&:to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_where_in(table, column, values)
|
||||||
|
return if values.blank?
|
||||||
|
return unless column_available?(table, column)
|
||||||
|
|
||||||
|
db.execute("DELETE FROM #{quote_table(table)} WHERE #{quote_column(column)} IN (#{values.map { |v| Integer(v) }.join(',')})")
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_by_column(table, column, value)
|
||||||
|
return unless column_available?(table, column)
|
||||||
|
|
||||||
|
db.execute("DELETE FROM #{quote_table(table)} WHERE #{quote_column(column)} = #{Integer(value)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def nullify_by_column(table, column, value)
|
||||||
|
return unless column_available?(table, column)
|
||||||
|
|
||||||
|
db.execute("UPDATE #{quote_table(table)} SET #{quote_column(column)} = NULL WHERE #{quote_column(column)} = #{Integer(value)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def column_available?(table, column)
|
||||||
|
db.data_source_exists?(table.to_s) && db.column_exists?(table.to_s, column.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_table(table)
|
||||||
|
db.quote_table_name(table)
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_column(column)
|
||||||
|
db.quote_column_name(column)
|
||||||
|
end
|
||||||
|
|
||||||
|
def db
|
||||||
|
ActiveRecord::Base.connection
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
DeleteObjectJob.prepend_mod_with('DeleteObjectJob')
|
DeleteObjectJob.prepend_mod_with('DeleteObjectJob')
|
||||||
|
|||||||
@ -32,6 +32,51 @@ RSpec.describe DeleteObjectJob, type: :job do
|
|||||||
expect(Contact.where(id: contact_ids).reload).not_to be_empty
|
expect(Contact.where(id: contact_ids).reload).not_to be_empty
|
||||||
expect { inbox.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { inbox.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'removes fazer.ai Captain and Jasmine inbox dependencies before destroying the inbox', :aggregate_failures do
|
||||||
|
contact = create(:contact, account: account)
|
||||||
|
contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox)
|
||||||
|
unit = create(:captain_unit, account: account)
|
||||||
|
assistant = create(:captain_assistant, account: account)
|
||||||
|
collection_id = insert_row(:jasmine_collections, account_id: account.id, name: 'Samanbaia', owner_inbox_id: inbox.id)
|
||||||
|
reservation_id = insert_row(
|
||||||
|
:captain_reservations,
|
||||||
|
account_id: account.id,
|
||||||
|
inbox_id: inbox.id,
|
||||||
|
contact_id: contact.id,
|
||||||
|
contact_inbox_id: contact_inbox.id,
|
||||||
|
suite_identifier: 'Suite 101',
|
||||||
|
check_in_at: 1.day.from_now,
|
||||||
|
check_out_at: 2.days.from_now,
|
||||||
|
status: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
unit.update!(inbox_id: inbox.id, concierge_inbox_id: inbox.id)
|
||||||
|
create(:captain_inbox, captain_assistant: assistant, inbox: inbox, captain_unit: unit)
|
||||||
|
create(:captain_conversation_insight, account: account, inbox: inbox)
|
||||||
|
create(:captain_gallery_item, :inbox_scoped, account: account, inbox: inbox, captain_unit: unit)
|
||||||
|
insert_row(:captain_lifecycle_deliveries, account_id: account.id, captain_reservation_id: reservation_id, inbox_id: inbox.id,
|
||||||
|
fire_at: 1.hour.from_now, status: 'scheduled', origin: 'scheduled_lifecycle')
|
||||||
|
insert_row(:captain_pix_charges, reservation_id: reservation_id, unit_id: unit.id, txid: SecureRandom.hex(8), status: 'active')
|
||||||
|
insert_row(:captain_inbox_automations, account_id: account.id, inbox_id: inbox.id, title: 'Menu', message: 'Oi')
|
||||||
|
insert_row(:captain_inbox_reminder_settings, account_id: account.id, inbox_id: inbox.id)
|
||||||
|
insert_row(:captain_notification_templates, inbox_id: inbox.id, label: 'Template', content: 'Oi')
|
||||||
|
insert_row(:captain_tool_configs, account_id: account.id, inbox_id: inbox.id, tool_key: 'availability')
|
||||||
|
insert_row(:jasmine_inbox_collections, account_id: account.id, inbox_id: inbox.id, collection_id: collection_id)
|
||||||
|
insert_row(:jasmine_inbox_settings, account_id: account.id, inbox_id: inbox.id)
|
||||||
|
insert_row(:jasmine_tool_configs, account_id: account.id, inbox_id: inbox.id, tool_key: 'availability')
|
||||||
|
|
||||||
|
expect { described_class.perform_now(inbox) }.not_to raise_error
|
||||||
|
|
||||||
|
expect(Inbox.exists?(inbox.id)).to be false
|
||||||
|
expect(CaptainInbox.where(inbox_id: inbox.id)).to be_empty
|
||||||
|
expect(Captain::Reservation.where(id: reservation_id)).to be_empty
|
||||||
|
expect(Captain::GalleryItem.where(inbox_id: inbox.id)).to be_empty
|
||||||
|
expect(Captain::ConversationInsight.where(inbox_id: inbox.id)).to be_empty
|
||||||
|
expect(unit.reload.inbox_id).to be_nil
|
||||||
|
expect(unit.concierge_inbox_id).to be_nil
|
||||||
|
expect(select_value(:jasmine_collections, collection_id, :owner_inbox_id)).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when object is heavy (Account)' do
|
context 'when object is heavy (Account)' do
|
||||||
@ -73,4 +118,29 @@ RSpec.describe DeleteObjectJob, type: :job do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insert_row(table, attributes)
|
||||||
|
attributes = attributes.merge(created_at: Time.current, updated_at: Time.current)
|
||||||
|
columns = attributes.keys
|
||||||
|
values = attributes.values.map { |value| quoted_value(value) }
|
||||||
|
sql = "INSERT INTO #{connection.quote_table_name(table)} (#{columns.map { |column| connection.quote_column_name(column) }.join(', ')}) " \
|
||||||
|
"VALUES (#{values.join(', ')}) RETURNING id"
|
||||||
|
|
||||||
|
connection.select_value(sql).to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_value(table, id, column)
|
||||||
|
connection.select_value(
|
||||||
|
"SELECT #{connection.quote_column_name(column)} FROM #{connection.quote_table_name(table)} WHERE id = #{Integer(id)}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def quoted_value(value)
|
||||||
|
value = value.to_json if value.is_a?(Hash) || value.is_a?(Array)
|
||||||
|
connection.quote(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def connection
|
||||||
|
ActiveRecord::Base.connection
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user