From 5ce77eedbe3f3b487cf9baf9bc142c61b4e60eb6 Mon Sep 17 00:00:00 2001 From: Muhsin <12408980+muhsin-k@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:33:43 +0400 Subject: [PATCH] feat(voice): embed Call in voice_call message payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a nested `call` object to the voice_call message JSON / push payload so the frontend can read call state directly from message.call instead of duplicating it in content_attributes.data. - Message gains has_one :call (inverse of existing Call#belongs_to :message) via enterprise module prepend. - Message serializer (_message.json.jbuilder) emits a call block for voice_call messages when the Call is linked. - Message#push_event_data mirrors the payload into socket broadcasts. - MessageFinder eager-loads the call (+ contact + inbox.channel) to avoid N+1 when listing conversation messages. - Call gains helper methods: display_status, from_number, to_number, recording_url, push_event_data. These centralize the direction- and status-aware derivations so jbuilder and push_event_data stay in sync. Frontend continues to read content_attributes.data — this commit only adds the new shape; a follow-up swaps readers to message.call. --- app/finders/message_finder.rb | 4 +- app/models/message.rb | 5 +++ .../api/v1/models/_message.json.jbuilder | 20 ++++++++++ enterprise/app/models/call.rb | 39 ++++++++++++++++++- enterprise/app/models/enterprise/message.rb | 8 ++++ 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb index 8854e239a..0695debc3 100644 --- a/app/finders/message_finder.rb +++ b/app/finders/message_finder.rb @@ -11,7 +11,9 @@ class MessageFinder private def conversation_messages - @conversation.messages.includes(:attachments, :sender, sender: { avatar_attachment: [:blob] }) + scope = @conversation.messages.includes(:attachments, :sender, sender: { avatar_attachment: [:blob] }) + scope = scope.includes(call: [:contact, { inbox: :channel }]) if Message.reflect_on_association(:call) + scope end def messages diff --git a/app/models/message.rb b/app/models/message.rb index f25d2e112..d1753f8d4 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -152,9 +152,14 @@ class Message < ApplicationRecord ) data[:echo_id] = echo_id if echo_id.present? data[:attachments] = attachments.map(&:push_event_data) if attachments.present? + data[:call] = call.push_event_data if voice_call? && respond_to?(:call) && call.present? merge_sender_attributes(data) end + def voice_call? + content_type == 'voice_call' + end + def conversation_push_event_data { assignee_id: conversation.assignee_id, diff --git a/app/views/api/v1/models/_message.json.jbuilder b/app/views/api/v1/models/_message.json.jbuilder index 583bc6fa6..b6c52a7b9 100644 --- a/app/views/api/v1/models/_message.json.jbuilder +++ b/app/views/api/v1/models/_message.json.jbuilder @@ -12,3 +12,23 @@ json.private message.private json.source_id message.source_id json.sender message.sender.push_event_data if message.sender json.attachments message.attachments.map(&:push_event_data) if message.attachments.present? + +if message.content_type == 'voice_call' && message.respond_to?(:call) && message.call.present? + call = message.call + json.call do + json.id call.id + json.provider_call_id call.provider_call_id + json.provider call.provider + json.direction call.direction + json.status call.display_status + json.duration_seconds call.duration_seconds + json.conference_sid call.conference_sid + json.accepted_by_agent_id call.accepted_by_agent_id + json.started_at call.started_at&.to_i + json.ended_at call.ended_at + json.from_number call.from_number + json.to_number call.to_number + json.recording_url call.recording_url + json.transcript call.transcript + end +end diff --git a/enterprise/app/models/call.rb b/enterprise/app/models/call.rb index b2346b4d5..853762785 100644 --- a/enterprise/app/models/call.rb +++ b/enterprise/app/models/call.rb @@ -45,7 +45,7 @@ class Call < ApplicationRecord belongs_to :inbox belongs_to :conversation belongs_to :contact - belongs_to :message, optional: true + belongs_to :message, optional: true, inverse_of: :call belongs_to :accepted_by_agent, class_name: 'User', optional: true has_one_attached :recording @@ -70,4 +70,41 @@ class Call < ApplicationRecord def display_direction DISPLAY_DIRECTION[direction] end + + def display_status + status.to_s.tr('_', '-') + end + + def from_number + incoming? ? contact.phone_number : inbox.channel&.phone_number + end + + def to_number + incoming? ? inbox.channel&.phone_number : contact.phone_number + end + + def recording_url + return nil unless recording.attached? + + Rails.application.routes.url_helpers.rails_blob_url(recording) + end + + def push_event_data + { + id: id, + provider_call_id: provider_call_id, + provider: provider, + direction: direction, + status: display_status, + duration_seconds: duration_seconds, + conference_sid: conference_sid, + accepted_by_agent_id: accepted_by_agent_id, + started_at: started_at&.to_i, + ended_at: ended_at, + from_number: from_number, + to_number: to_number, + recording_url: recording_url, + transcript: transcript + } + end end diff --git a/enterprise/app/models/enterprise/message.rb b/enterprise/app/models/enterprise/message.rb index 4b08a4ed5..4b89b6b99 100644 --- a/enterprise/app/models/enterprise/message.rb +++ b/enterprise/app/models/enterprise/message.rb @@ -1,4 +1,12 @@ module Enterprise::Message + def self.prepended(base) + base.class_eval do + has_one :call, class_name: 'Call', foreign_key: :message_id, dependent: :nullify, inverse_of: :message + + scope :with_call, -> { includes(call: [:contact, { inbox: :channel }]) } + end + end + private def mark_pending_conversation_as_open_for_human_response