feat: Add unified Call model for voice calling (#14026)
Adds a Call model to track voice call state across providers (Twilio, WhatsApp). This replaces storing call data in conversation.additional_attributes and provides a foundation for call analytics multi-call-per-conversation support, and future voice providers. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This commit is contained in:
@@ -450,3 +450,4 @@ class Message < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
Message.prepend_mod_with('Message')
|
Message.prepend_mod_with('Message')
|
||||||
|
Message.include_mod_with('Concerns::Message')
|
||||||
|
|||||||
34
db/migrate/20260408170902_create_calls.rb
Normal file
34
db/migrate/20260408170902_create_calls.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class CreateCalls < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :calls do |t|
|
||||||
|
t.bigint :account_id, null: false
|
||||||
|
t.bigint :inbox_id, null: false
|
||||||
|
t.bigint :conversation_id, null: false
|
||||||
|
t.bigint :contact_id, null: false
|
||||||
|
t.bigint :message_id
|
||||||
|
t.bigint :accepted_by_agent_id
|
||||||
|
t.string :provider_call_id, null: false
|
||||||
|
t.integer :provider, null: false, default: 0
|
||||||
|
t.integer :direction, null: false
|
||||||
|
t.string :status, null: false, default: 'ringing'
|
||||||
|
t.datetime :started_at
|
||||||
|
t.integer :duration_seconds
|
||||||
|
t.string :end_reason
|
||||||
|
t.jsonb :meta, default: {}
|
||||||
|
t.text :transcript
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_call_indexes
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def add_call_indexes
|
||||||
|
add_index :calls, [:provider, :provider_call_id], unique: true
|
||||||
|
add_index :calls, [:account_id, :conversation_id]
|
||||||
|
add_index :calls, [:account_id, :contact_id]
|
||||||
|
add_index :calls, :message_id
|
||||||
|
end
|
||||||
|
end
|
||||||
24
db/schema.rb
24
db/schema.rb
@@ -261,6 +261,30 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_09_091202) do
|
|||||||
t.index ["account_id"], name: "index_automation_rules_on_account_id"
|
t.index ["account_id"], name: "index_automation_rules_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "calls", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.bigint "inbox_id", null: false
|
||||||
|
t.bigint "conversation_id", null: false
|
||||||
|
t.bigint "contact_id", null: false
|
||||||
|
t.bigint "message_id"
|
||||||
|
t.bigint "accepted_by_agent_id"
|
||||||
|
t.string "provider_call_id", null: false
|
||||||
|
t.integer "provider", default: 0, null: false
|
||||||
|
t.integer "direction", null: false
|
||||||
|
t.string "status", default: "ringing", null: false
|
||||||
|
t.datetime "started_at"
|
||||||
|
t.integer "duration_seconds"
|
||||||
|
t.string "end_reason"
|
||||||
|
t.jsonb "meta", default: {}
|
||||||
|
t.text "transcript"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id", "contact_id"], name: "index_calls_on_account_id_and_contact_id"
|
||||||
|
t.index ["account_id", "conversation_id"], name: "index_calls_on_account_id_and_conversation_id"
|
||||||
|
t.index ["message_id"], name: "index_calls_on_message_id"
|
||||||
|
t.index ["provider", "provider_call_id"], name: "index_calls_on_provider_and_provider_call_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "campaigns", force: :cascade do |t|
|
create_table "campaigns", force: :cascade do |t|
|
||||||
t.integer "display_id", null: false
|
t.integer "display_id", null: false
|
||||||
t.string "title", null: false
|
t.string "title", null: false
|
||||||
|
|||||||
55
enterprise/app/models/call.rb
Normal file
55
enterprise/app/models/call.rb
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: calls
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# direction :integer not null
|
||||||
|
# duration_seconds :integer
|
||||||
|
# end_reason :string
|
||||||
|
# meta :jsonb
|
||||||
|
# provider :integer default("twilio"), not null
|
||||||
|
# started_at :datetime
|
||||||
|
# status :string default("ringing"), not null
|
||||||
|
# transcript :text
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# accepted_by_agent_id :bigint
|
||||||
|
# account_id :bigint not null
|
||||||
|
# contact_id :bigint not null
|
||||||
|
# conversation_id :bigint not null
|
||||||
|
# inbox_id :bigint not null
|
||||||
|
# message_id :bigint
|
||||||
|
# provider_call_id :string not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_calls_on_account_id_and_contact_id (account_id,contact_id)
|
||||||
|
# index_calls_on_account_id_and_conversation_id (account_id,conversation_id)
|
||||||
|
# index_calls_on_message_id (message_id)
|
||||||
|
# index_calls_on_provider_and_provider_call_id (provider,provider_call_id) UNIQUE
|
||||||
|
#
|
||||||
|
class Call < ApplicationRecord
|
||||||
|
# All valid call statuses
|
||||||
|
STATUSES = %w[ringing in_progress completed no_answer failed].freeze
|
||||||
|
# Statuses where the call is finished and won't change again
|
||||||
|
TERMINAL_STATUSES = %w[completed no_answer failed].freeze
|
||||||
|
|
||||||
|
enum :provider, { twilio: 0, whatsapp: 1 }
|
||||||
|
enum :direction, { incoming: 0, outgoing: 1 }
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :inbox
|
||||||
|
belongs_to :conversation
|
||||||
|
belongs_to :contact
|
||||||
|
belongs_to :message, optional: true
|
||||||
|
belongs_to :accepted_by_agent, class_name: 'User', optional: true
|
||||||
|
|
||||||
|
has_one_attached :recording
|
||||||
|
|
||||||
|
validates :provider_call_id, presence: true
|
||||||
|
validates :provider, presence: true
|
||||||
|
validates :direction, presence: true
|
||||||
|
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||||
|
|
||||||
|
scope :active, -> { where.not(status: TERMINAL_STATUSES) }
|
||||||
|
end
|
||||||
@@ -17,6 +17,7 @@ module Enterprise::Concerns::Account
|
|||||||
has_many :copilot_threads, dependent: :destroy_async
|
has_many :copilot_threads, dependent: :destroy_async
|
||||||
has_many :companies, dependent: :destroy_async
|
has_many :companies, dependent: :destroy_async
|
||||||
has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice'
|
has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice'
|
||||||
|
has_many :calls, dependent: :destroy_async
|
||||||
|
|
||||||
has_one :saml_settings, dependent: :destroy_async, class_name: 'AccountSamlSettings'
|
has_one :saml_settings, dependent: :destroy_async, class_name: 'AccountSamlSettings'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module Enterprise::Concerns::Conversation
|
|||||||
belongs_to :sla_policy, optional: true
|
belongs_to :sla_policy, optional: true
|
||||||
has_one :applied_sla, dependent: :destroy_async
|
has_one :applied_sla, dependent: :destroy_async
|
||||||
has_many :sla_events, dependent: :destroy_async
|
has_many :sla_events, dependent: :destroy_async
|
||||||
|
has_many :calls, dependent: :destroy_async
|
||||||
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
|
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
|
||||||
before_validation :validate_sla_policy, if: -> { sla_policy_id_changed? }
|
before_validation :validate_sla_policy, if: -> { sla_policy_id_changed? }
|
||||||
around_save :ensure_applied_sla_is_created, if: -> { sla_policy_id_changed? }
|
around_save :ensure_applied_sla_is_created, if: -> { sla_policy_id_changed? }
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ module Enterprise::Concerns::Inbox
|
|||||||
through: :captain_inbox,
|
through: :captain_inbox,
|
||||||
class_name: 'Captain::Assistant'
|
class_name: 'Captain::Assistant'
|
||||||
has_many :inbox_capacity_limits, dependent: :destroy
|
has_many :inbox_capacity_limits, dependent: :destroy
|
||||||
|
has_many :calls, dependent: :destroy_async
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
7
enterprise/app/models/enterprise/concerns/message.rb
Normal file
7
enterprise/app/models/enterprise/concerns/message.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module Enterprise::Concerns::Message
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
has_one :call, dependent: :nullify
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user