chore: random clean up

This commit is contained in:
Sojan
2025-05-04 09:17:03 -07:00
parent 2f7c8f6cfc
commit 5dc1735f69
5 changed files with 178 additions and 65 deletions
+113 -39
View File
@@ -63,19 +63,23 @@ module Voice
}.freeze
# Provider-specific message templates for different call statuses
# These messages must EXACTLY match what the UI expects to show
PROVIDER_MESSAGES = {
twilio: {
'initiated' => { outbound: 'Outbound call initiated', inbound: 'Initiating call' },
'ringing' => { outbound: 'Phone ringing', inbound: 'Phone ringing' },
# Outgoing calls
'initiated' => { outbound: 'Call started…', inbound: 'Incoming call…' },
'ringing' => { outbound: 'Call started…', inbound: 'Incoming call…' },
'in-progress' => {
outbound: { first: 'Call connected', next: 'Call in progress' },
inbound: { first: 'Call answered', next: 'Call in progress' }
outbound: { first: 'Call in progress…', next: 'Call in progress' },
inbound: { first: 'Call in progress…', next: 'Call in progress' }
},
'completed' => { outbound: 'Call completed', inbound: 'Call completed' },
'busy' => { outbound: 'Call busy', inbound: 'Call busy' },
'failed' => { outbound: 'Call failed', inbound: 'Call failed' },
'no-answer' => { outbound: 'Call not answered', inbound: 'Call not answered' },
'canceled' => { outbound: 'Call canceled', inbound: 'Call canceled' }
'active' => { outbound: 'Call in progress…', inbound: 'Call in progress…' },
'completed' => { outbound: 'Call ended', inbound: 'Call ended' },
'busy' => { outbound: 'Line busy', inbound: 'Missed call' }, # Show as missed for inbound
'failed' => { outbound: 'Call failed', inbound: 'Missed call' },
'no-answer' => { outbound: 'No answer', inbound: 'Missed call' },
'canceled' => { outbound: 'Call canceled', inbound: 'Missed call' },
'missed' => { outbound: 'Call missed', inbound: 'Missed call' }
}
}.freeze
@@ -103,16 +107,33 @@ module Voice
# @param is_first_response [Boolean] Whether this is the first status update for this status
# @return [Boolean] Whether the update was processed successfully
def process_status_update(status, duration = nil, is_first_response = false)
# Normalize status using the STATUS_MAPPING if present
normalized_status = STATUS_MAPPING[status] || status
# Get current status
prev_status = conversation.additional_attributes&.dig('call_status')
# Skip if no changes needed to avoid duplicate processing
# Unless this is marked as the first response, which we should always process
prev_status = conversation.additional_attributes&.dig('call_status')
if !is_first_response && prev_status == status
Rails.logger.info("🔄 [CallStatusManager] Skipping duplicate status update: '#{status}'")
if !is_first_response && prev_status == normalized_status
Rails.logger.info("🔄 [CallStatusManager] Skipping duplicate status update: '#{normalized_status}'")
return true
end
# Normalize status using the STATUS_MAPPING if present
normalized_status = STATUS_MAPPING[status] || status
# Handle status transitions - only allow certain transitions
if prev_status.present? && !is_first_response
# Don't move backwards in the status flow unless forced to
if prev_status == 'in-progress' && normalized_status == 'ringing'
Rails.logger.info("🔄 [CallStatusManager] Skipping backward transition: '#{prev_status}' -> '#{normalized_status}'")
return true
end
# Don't override a completed status with in-progress
if TERMINAL_STATUSES.include?(prev_status) && normalized_status == 'in-progress'
Rails.logger.info("🔄 [CallStatusManager] Call already ended, skipping update to: '#{normalized_status}'")
return true
end
end
# Calculate call duration automatically if not provided and call is ending
if duration.nil? && call_ended?(normalized_status) && conversation.additional_attributes['call_started_at']
@@ -154,6 +175,36 @@ module Voice
# Most calls are inbound, so this is a reasonable default
false
end
# Convert internal status to UI-friendly status
# For consistent display across all parts of the UI
# This is used by other services to ensure consistent status display
# @param status [String] The raw status to normalize
# @return [String] The UI-friendly status value
def normalized_ui_status(status)
incoming = !is_outbound?
case status
when 'initiated', 'ringing'
is_outbound? ? 'started' : 'ringing'
when 'in-progress', 'active'
'in_progress'
when 'completed', 'ended'
'ended'
when 'missed'
is_outbound? ? 'no_answer' : 'missed'
when 'busy'
is_outbound? ? 'busy' : 'missed' # Treat busy as missed for incoming
when 'failed'
is_outbound? ? 'failed' : 'missed' # Treat failed as missed for incoming
when 'no-answer'
is_outbound? ? 'no_answer' : 'missed' # For incoming, this is missed
when 'canceled'
is_outbound? ? 'canceled' : 'missed' # Treat canceled as missed for incoming
else
is_outbound? ? 'ended' : 'missed' # Default to missed for incoming if we don't know
end
end
# Generate provider-specific activity messages (e.g., for Twilio)
def create_provider_activity_message(status, is_first_response = false)
@@ -240,6 +291,9 @@ module Voice
conversation.additional_attributes ||= {}
conversation.additional_attributes['call_status'] = status
# Also store the UI-friendly status for consistent display
conversation.additional_attributes['ui_call_status'] = normalized_ui_status(status)
# Update timestamps and metadata based on status
if %w[in-progress active].include?(status)
# Record the start time if not already set
@@ -273,23 +327,18 @@ module Voice
message = find_voice_call_message
return unless message
# Determine best message status value based on conversation status
message_status = status
if status == 'in-progress'
message_status = 'active'
elsif call_ended?(status)
message_status = 'ended'
end
# Use the normalized UI status for consistent display
ui_status = normalized_ui_status(status)
# Get current content attributes, initialize if needed
content_attributes = message.content_attributes || {}
content_attributes['data'] ||= {}
# Update fields
content_attributes['data']['status'] = message_status
content_attributes['data']['status'] = ui_status
content_attributes['data']['duration'] = duration if duration
content_attributes['data']['meta'] ||= {}
content_attributes['data']['meta']["#{message_status}_at"] = Time.now.to_i
content_attributes['data']['meta']["#{ui_status}_at"] = Time.now.to_i
content_attributes['data']['updated_at'] = Time.now.to_i
# Add a flag to force the UI to refresh
@@ -343,25 +392,43 @@ module Voice
end
def create_status_activity_message(status)
# Use messages that match the UI expectations exactly
# These messages will appear in the activity feed
content = if call_ended?(status)
case status
when 'missed'
'Call was not answered'
when 'busy'
'Line was busy'
when 'failed'
'Call failed'
when 'no-answer'
'No answer'
when 'canceled'
'Call was canceled'
if !is_outbound?
# All ended incoming calls should show as "Missed call" if they weren't answered
# Only show "Call ended" if the call was actually answered (in progress)
conversation_was_active = conversation.additional_attributes['call_started_at'].present?
if conversation_was_active
'Call ended'
else
'Missed call' # Default for all incoming ended calls that weren't answered
end
else
'Call ended'
# For outbound calls, show appropriate endings
case status
when 'missed'
'No answer'
when 'busy'
'Line busy'
when 'failed'
'Call failed'
when 'no-answer'
'No answer'
when 'canceled'
'Call canceled'
else
'Call ended'
end
end
elsif status == 'in-progress'
'Call in progress'
elsif %w[in-progress active].include?(status)
'Call in progress'
elsif status == 'ringing'
is_outbound? ? 'Call started…' : 'Incoming call…'
elsif status == 'initiated'
is_outbound? ? 'Call started…' : 'Incoming call…'
else
"Call status changed to #{status}"
"Call status: #{status}"
end
# Use the public create_activity_message method
@@ -376,13 +443,19 @@ module Voice
Rails.logger.info("📢 [CallStatusManager] Broadcasting status change: '#{status}' for conversation_id=#{conversation.id}")
# Use account-level channel for maximum compatibility
# Convert the internal status to the UI-friendly format
# This ensures the conversation list shows consistent status texts
ui_status = normalized_ui_status(status)
Rails.logger.info("📢 [CallStatusManager] Broadcasting UI status: '#{ui_status}' for conversation_id=#{conversation.id}")
ActionCable.server.broadcast(
"account_#{conversation.account_id}",
{
event_name: 'call_status_changed',
data: {
call_sid: call_sid,
status: status,
status: ui_status, # Send UI-friendly status
conversation_id: conversation.id,
inbox_id: conversation.inbox_id,
timestamp: Time.now.to_i
@@ -390,6 +463,7 @@ module Voice
}
)
end
end
end
end
@@ -178,22 +178,24 @@ module Voice
conversation.additional_attributes['agent_joined_at'] = Time.now.to_i
Rails.logger.info("👤 AGENT JOINED AT: #{Time.now.to_i}")
# If call is ringing when agent joins, mark as active
# If call is ringing when agent joins, mark as connected
return unless conversation.additional_attributes['call_status'] == 'ringing'
Rails.logger.info('📞 UPDATING RINGING CALL TO ACTIVE (agent joined)')
call_status_manager.process_status_update('active')
Rails.logger.info('📞 UPDATING RINGING CALL TO CONNECTED (agent joined)')
# Always use in-progress to be consistent with status mapping
call_status_manager.process_status_update('in-progress', nil, true)
end
def handle_caller_join
conversation.additional_attributes['caller_joined_at'] = Time.now.to_i
Rails.logger.info("👤 CALLER JOINED AT: #{Time.now.to_i}")
# For outbound calls - mark as active when caller joins if still ringing
# For outbound calls - mark as connected when caller joins if still ringing
return unless outbound_call? && ringing_call?
Rails.logger.info('📞 UPDATING RINGING OUTBOUND CALL TO ACTIVE (caller joined)')
call_status_manager.process_status_update('active')
Rails.logger.info('📞 UPDATING RINGING OUTBOUND CALL TO CONNECTED (caller joined)')
# Always use in-progress to be consistent with status mapping
call_status_manager.process_status_update('in-progress', nil, true)
end
def handle_generic_participant_join
@@ -202,8 +204,9 @@ module Voice
# If we're stuck in ringing for a while, try to move forward
return unless ringing_call? && long_ringing?
Rails.logger.info('📞 UPDATING LONG-RINGING CALL TO ACTIVE (participant joined)')
call_status_manager.process_status_update('active')
Rails.logger.info('📞 UPDATING LONG-RINGING CALL TO CONNECTED (participant joined)')
# Always use in-progress to be consistent with status mapping
call_status_manager.process_status_update('in-progress', nil, true)
end
# Call state checks
@@ -225,10 +228,12 @@ module Voice
both_present = conversation.additional_attributes['agent_joined_at'] &&
conversation.additional_attributes['caller_joined_at']
return unless both_present && conversation.additional_attributes['call_status'] != 'active'
# Only update if not already in an active state
return unless both_present && !%w[active in-progress].include?(conversation.additional_attributes['call_status'])
Rails.logger.info('📞 UPDATING CALL STATUS TO ACTIVE (both parties present)')
call_status_manager.process_status_update('active')
Rails.logger.info('📞 UPDATING CALL STATUS TO CONNECTED (both parties present)')
# Always use in-progress to be consistent with status mapping
call_status_manager.process_status_update('in-progress', nil, true)
end
# Missed call check when caller leaves
@@ -295,23 +300,38 @@ module Voice
# Activity message creation
def create_activity_message
content = activity_message_content_for_event
call_status_manager.create_activity_message(content)
# Only create message if we have content to show
call_status_manager.create_activity_message(content) if content.present?
end
def activity_message_content_for_event
case event
when CONFERENCE_START
'Conference started'
# Don't show this message to avoid redundancy with call status messages
nil
when CONFERENCE_END
'Conference ended'
# Don't show this message to avoid redundancy with call status messages
nil
when PARTICIPANT_JOIN
participant_type = agent_participant? ? 'Agent' : 'Caller'
"#{participant_type} joined the call"
if agent_participant?
"Agent joined the call"
elsif caller_participant?
# Only for inbound calls - creates "Caller joined the call" message
# For outbound calls, we don't need this as we show "Call connected" instead
call_status_manager.is_outbound? ? nil : "Caller joined the call"
else
nil # Don't show for generic participants
end
when PARTICIPANT_LEAVE
participant_type = agent_participant? ? 'Agent' : 'Caller'
"#{participant_type} left the call"
if agent_participant?
"Agent left the call"
elsif caller_participant?
"Caller left the call"
else
nil # Don't show for generic participants
end
else
"Call event: #{event}"
nil # Don't show unknown events
end
end
end
@@ -68,7 +68,6 @@ module Voice
def initial_attributes
attributes = {
'call_status' => 'in-progress',
'call_initiated_at' => Time.now.to_i
}
+13 -3
View File
@@ -81,7 +81,8 @@ module Voice
Rails.logger.info("📬 CONTACT INBOX: id=#{@contact_inbox.id} source_id=#{@contact_inbox.source_id}")
# Create a new conversation with call details
# Create a new conversation with basic call details
# Status will be properly set by CallStatusManager later
@conversation = account.conversations.create!(
contact_inbox_id: @contact_inbox.id,
inbox_id: @inbox.id,
@@ -89,7 +90,6 @@ module Voice
status: :open,
additional_attributes: {
'call_sid' => caller_info[:call_sid],
'call_status' => 'ringing',
'call_direction' => 'inbound',
'call_initiated_at' => Time.now.to_i,
'call_type' => 'inbound'
@@ -106,6 +106,16 @@ module Voice
def create_voice_call_message
# Create a single voice call message from contact for this call
# Initialize CallStatusManager to get normalized status
status_manager = Voice::CallStatus::Manager.new(
conversation: @conversation,
call_sid: caller_info[:call_sid],
provider: :twilio
)
# Get UI-friendly status for consistent display
ui_status = status_manager.normalized_ui_status('ringing')
message_params = {
content: 'Voice Call',
message_type: 'incoming',
@@ -113,7 +123,7 @@ module Voice
content_attributes: {
data: {
call_sid: caller_info[:call_sid],
status: 'ringing',
status: ui_status, # Use normalized UI status
conversation_id: @conversation.id,
call_direction: 'inbound',
conference_sid: @conversation.additional_attributes['conference_sid'],
+13 -3
View File
@@ -50,10 +50,10 @@ module Voice
agent_id: user.id # Pass the agent ID to track who initiated the call
)
# Update conversation with call details
# Update conversation with call details, but don't set status
# Status will be properly set by CallStatusManager
updated_attributes = @conversation.additional_attributes.merge({
'call_sid' => @call_details[:call_sid],
'call_status' => 'in-progress',
'requires_agent_join' => true,
'agent_id' => user.id # Store the agent ID who initiated the call
})
@@ -63,6 +63,16 @@ module Voice
def create_voice_call_message
# Create a voice call message
# Initialize CallStatusManager to get normalized status
status_manager = Voice::CallStatus::Manager.new(
conversation: @conversation,
call_sid: @call_details[:call_sid],
provider: :twilio
)
# Get UI-friendly status
ui_status = status_manager.normalized_ui_status('ringing')
message_params = {
content: 'Voice Call',
message_type: 'outgoing',
@@ -70,7 +80,7 @@ module Voice
content_attributes: {
data: {
call_sid: @call_details[:call_sid],
status: 'ringing',
status: ui_status, # Set the normalized UI status
conversation_id: @conversation.id,
call_direction: 'outbound',
conference_sid: @conference_name,