diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 911cfdac6..29de6be22 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -108,6 +108,7 @@ class Conversation < ApplicationRecord has_many :mentions, dependent: :destroy_async has_many :messages, dependent: :destroy_async, autosave: true + has_many :calls, dependent: :destroy_async has_one :csat_survey_response, dependent: :destroy_async has_many :conversation_participants, dependent: :destroy_async has_many :notifications, as: :primary_actor, dependent: :destroy_async diff --git a/db/migrate/20260408170902_create_calls.rb b/db/migrate/20260408170902_create_calls.rb new file mode 100644 index 000000000..c6a4055e3 --- /dev/null +++ b/db/migrate/20260408170902_create_calls.rb @@ -0,0 +1,26 @@ +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 :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.integer :duration_seconds + t.string :end_reason + t.jsonb :meta, default: {} + t.text :transcript + + t.timestamps + end + + add_index :calls, [:provider, :provider_call_id], unique: true + add_index :calls, [:account_id, :conversation_id] + add_index :calls, :message_id + add_index :calls, :meta, using: :gin + end +end diff --git a/db/schema.rb b/db/schema.rb index d0993a55b..e00b690a0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_03_24_102005) do +ActiveRecord::Schema[7.1].define(version: 2026_04_08_170902) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -261,6 +261,28 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_24_102005) do t.index ["account_id"], name: "index_automation_rules_on_account_id" 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 "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.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", "conversation_id"], name: "index_calls_on_account_id_and_conversation_id" + t.index ["message_id"], name: "index_calls_on_message_id" + t.index ["meta"], name: "index_calls_on_meta", using: :gin + 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| t.integer "display_id", null: false t.string "title", null: false diff --git a/enterprise/app/models/call.rb b/enterprise/app/models/call.rb new file mode 100644 index 000000000..33ae10eb1 --- /dev/null +++ b/enterprise/app/models/call.rb @@ -0,0 +1,24 @@ +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 :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 diff --git a/enterprise/app/models/enterprise/concerns/account.rb b/enterprise/app/models/enterprise/concerns/account.rb index 01693ac79..427b5245b 100644 --- a/enterprise/app/models/enterprise/concerns/account.rb +++ b/enterprise/app/models/enterprise/concerns/account.rb @@ -17,6 +17,7 @@ module Enterprise::Concerns::Account has_many :copilot_threads, dependent: :destroy_async has_many :companies, dependent: :destroy_async 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' end diff --git a/twilio_qa.md b/twilio_qa.md new file mode 100644 index 000000000..457f9a4ed --- /dev/null +++ b/twilio_qa.md @@ -0,0 +1,56 @@ +# Voice as Twilio SMS Capability — QA Test Cases +## Prerequisites + +* Enterprise edition +* `channel_voice` feature flag enabled: `Account.find().enable_features('channel_voice'); Account.find().save!` +* Twilio account with a voice-capable phone number +* Twilio API Key SID + API Key Secret + +--- + +## Voice inbox creation + +1. ~~Create Voice inbox via Voice tile with valid credentials — Channel created with voice_enabled: true, twiml_app_sid auto-provisioned, SMS and calls work~~ +2. ~~Create Voice inbox with a number that doesn't support voice (e.g., short code) — Fails with "This phone number does not support voice calls" error~~ +3. ~~Create Voice inbox with invalid Twilio credentials — Fails with clear error message~~ + +## Enable voice on existing SMS inbox + + 5. ~~SMS inbox (phone number, no ~~`~~api_key_sid~~`~~) → enable voice — Prompts for both ~~`~~api_key_sid~~`~~ and ~~`~~api_key_secret~~`~~, provisions TwiML app~~ + 6. ~~SMS inbox (phone number, has ~~`~~api_key_sid~~`~~, no ~~`~~api_key_secret~~`~~) → enable voice — Prompts for ~~`~~api_key_secret~~`~~ only, provisions TwiML app~~ + 7. ~~SMS inbox (phone number, has api_key_sid + api_key_secret) → enable voice — No credential fields, just toggle and update~~ + 8. ~~SMS inbox (messaging_service_sid only) — Voice tab not shown (voice requires phone number)~~ + 9. ~~Twilio WhatsApp inbox — Voice tab not shown (~~`~~medium !== 'sms'~~`~~)~~ +10. ~~SMS inbox without ~~`~~channel_voice~~`~~ feature flag — Voice tab not shown~~ +11. ~~Enable voice via API when ~~`~~channel_voice~~`~~ flag is off — Voice fields silently ignored~~ + +## Disable voice + +12. ~~SMS+Voice channel → disable voice — TwiML app deleted from Twilio, ~~`~~twiml_app_sid~~`~~ cleared, ~~`~~api_key_sid~~`~~ and ~~`~~api_key_secret~~`~~ preserved, SMS still works~~ +13. ~~Disable voice when Twilio API fails (e.g., TwiML app already deleted) — Local cleanup still happens, no error shown to user~~ + +## Re-enable voice + +14. ~~Previously disabled voice channel (has api_key_sid + api_key_secret) → re-enable — No credentials needed, new TwiML app provisioned~~ +15. ~~Previously disabled voice channel (has api_key_sid, api_key_secret is nil) → re-enable — Prompts for api_key_secret~~ + +## Voice settings tab UI + +16. ~~Voice enabled + configured — Shows toggle (on) + voice webhook URLs~~ +17. ~~Voice disabled + credentials exist — Shows toggle (off) only, no credential fields~~ +18. ~~Voice disabled + no credentials → toggle on — Shows credential fields when toggled on~~ +19. ~~Click Update button — Shows spinner while saving~~ +20. ~~After update completes — Tab stays selected (doesn't jump to another tab)~~ +21. ~~Voice-enabled inbox still shows Business Hours, CSAT, Help Center, and Configuration tabs~~ + +## Inbox list and naming + +22. ~~Voice-enabled TwilioSms inbox in inbox list — Shows "Voice" as channel name~~ +23. ~~Voice-disabled TwilioSms inbox in inbox list — Shows "Twilio SMS" as channel name~~ +24. ~~Voice-enabled inbox icon — Shows voice icon (phone)~~ + +## Inbox creation UI + +25. ~~Voice tile shows "Coming Soon" when ~~`~~channel_voice~~`~~ feature flag is off~~ +26. ~~Voice tile is clickable when ~~`~~channel_voice~~`~~ feature flag is on~~ +27. ~~Voice tile description reads "Handle voice calls and SMS with Twilio"~~