From beef43f2bf0b17618a82abda92ef253ae4c25228 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Thu, 30 Apr 2026 18:04:25 +0700 Subject: [PATCH] refactor(voice): hoist validations to before_action and centralize call-error rendering --- .../v1/accounts/whatsapp_calls_controller.rb | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb index 61f3f9cea..990f5188d 100644 --- a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb @@ -3,60 +3,46 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro before_action :set_call, only: %i[show accept reject terminate upload_recording] before_action :set_conversation, only: :initiate + before_action :validate_recording, only: :upload_recording + before_action :validate_initiate, only: :initiate + + rescue_from Voice::CallErrors::NotRinging, + Voice::CallErrors::AlreadyAccepted, + Voice::CallErrors::CallFailed, + with: :render_call_error def show; end def accept - @call = Whatsapp::CallService.new(call: @call, agent: Current.user, sdp_answer: params[:sdp_answer]).accept - rescue Voice::CallErrors::NotRinging, Voice::CallErrors::AlreadyAccepted, Voice::CallErrors::CallFailed => e - render_could_not_create_error(e.message) + @call = call_service.accept end def reject - @call = Whatsapp::CallService.new(call: @call, agent: Current.user).reject - rescue Voice::CallErrors::CallFailed => e - render_could_not_create_error(e.message) + @call = call_service.reject end def terminate - @call = Whatsapp::CallService.new(call: @call, agent: Current.user).terminate - rescue Voice::CallErrors::CallFailed => e - render_could_not_create_error(e.message) + @call = call_service.terminate end - # Browser-supplied recording captured via MediaRecorder. Idempotent: the - # check-and-create runs under the message row lock so the hangup-vs-pagehide - # race can't double-attach. def upload_recording - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.no_recording')) if params[:recording].blank? - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.no_message')) if @call.message.blank? - - @call.message.with_lock do - @upload_status = if @call.message.attachments.exists?(file_type: :audio) - 'already_uploaded' - else - @call.message.attachments.create!(account_id: @call.account_id, file_type: :audio, file: params[:recording]) - 'uploaded' - end - end + @upload_status = @call.message.with_lock { attach_recording_idempotently } end def initiate - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled')) unless calling_enabled?(@conversation) - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.sdp_offer_required')) if params[:sdp_offer].blank? - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.contact_phone_required')) if @conversation.contact&.phone_number.blank? - @call = create_outbound_call(@conversation, params[:sdp_offer]) @message = Voice::CallMessageBuilder.new(@call).perform! @call.update!(message_id: @message.id) rescue Voice::CallErrors::NoCallPermission handle_no_call_permission(@conversation) - rescue Voice::CallErrors::CallFailed => e - render_could_not_create_error(e.message) end private + def call_service + @call_service ||= Whatsapp::CallService.new(call: @call, agent: Current.user, sdp_answer: params[:sdp_answer]) + end + def set_call @call = Current.account.calls.whatsapp.find(params[:id]) authorize @call.conversation, :show? @@ -67,6 +53,19 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro authorize @conversation, :show? end + def validate_recording + render_could_not_create_error(I18n.t('errors.whatsapp.calls.no_recording')) and return if params[:recording].blank? + + render_could_not_create_error(I18n.t('errors.whatsapp.calls.no_message')) if @call.message.blank? + end + + def validate_initiate + render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled')) and return unless calling_enabled?(@conversation) + render_could_not_create_error(I18n.t('errors.whatsapp.calls.sdp_offer_required')) and return if params[:sdp_offer].blank? + + render_could_not_create_error(I18n.t('errors.whatsapp.calls.contact_phone_required')) if @conversation.contact&.phone_number.blank? + end + # WhatsApp Cloud Calling specifically — Twilio voice channels also expose # voice_enabled? but use a different (Voice::OutboundCallBuilder) initiation path. def calling_enabled?(conversation) @@ -74,8 +73,15 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro channel.is_a?(Channel::Whatsapp) && channel.voice_enabled? end + def attach_recording_idempotently + return 'already_uploaded' if @call.message.attachments.exists?(file_type: :audio) + + @call.message.attachments.create!(account_id: @call.account_id, file_type: :audio, file: params[:recording]) + 'uploaded' + end + # Browser-built SDP offer is forwarded to Meta; the connect webhook later delivers Meta's answer. - # Caller must validate `conversation.contact.phone_number` is present (initiate's guard does). + # validate_initiate ensures conversation.contact.phone_number is present. def create_outbound_call(conversation, sdp_offer) contact_phone = conversation.contact.phone_number result = conversation.inbox.channel.provider_service.initiate_call(contact_phone.delete('+'), sdp_offer) @@ -109,4 +115,8 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro conversation.update!(additional_attributes: attrs) render json: { status: 'permission_requested' } end + + def render_call_error(error) + render_could_not_create_error(error.message) + end end