A forms/configure_calls.rb => forms/configure_calls.rb +48 -0
@@ 0,0 1,48 @@
+form!
+title "Configure Calls"
+
+field(
+ var: "fwd[timeout]",
+ type: "text-single",
+ datatype: "xs:integer",
+ label: "Seconds to ring before voicemail",
+ description: "One ring is ~5 seconds. Negative means ring forever.",
+ value: @customer.fwd.timeout.to_i.to_s
+)
+
+field(
+ var: "voicemail_transcription",
+ type: "boolean",
+ label: "Voicemail transcription",
+ value: @customer.transcription_enabled.to_s
+)
+
+field(
+ var: "fwd[uri]",
+ type: "list-single",
+ datatype: "xs:anyURI",
+ open: true,
+ label: "Forward calls to",
+ description: "List item or any custom xmpp:, sip:, or tel: URI.",
+ options: [
+ { label: "Jabber ID", value: "xmpp:#{@customer.jid}" },
+ { label: "SIP Account", value: @customer.sip_account.uri }
+ ],
+ value: @customer.fwd.uri
+)
+
+if @customer.tndetails.dig(:features, :lidb)
+ field(
+ var: "lidb_name",
+ type: "fixed",
+ label: "CNAM",
+ value: "#{@lidb[:name]} (#{@lidb[:status]})"
+ )
+elsif @customer.tndetails[:on_net_vendor]
+ field(
+ var: "lidb_name",
+ type: "text-single",
+ label: "CNAM Name",
+ description: "or nothing/space to leave blank"
+ )
+end
M lib/backend_sgx.rb => lib/backend_sgx.rb +0 -8
@@ 36,14 36,6 @@ class BackendSgx
end
end
- def set_fwd(uri)
- REDIS.set("catapult_fwd-#{registered?.phone}", uri)
- end
-
- def set_fwd_timeout(timeout)
- REDIS.set("catapult_fwd_timeout-#{from_jid}", timeout)
- end
-
def set_ogm_url(url)
REDIS.set("catapult_ogm_url-#{from_jid}", url)
end
M lib/bwmsgsv2_repo.rb => lib/bwmsgsv2_repo.rb +29 -1
@@ 20,15 20,43 @@ class Bwmsgsv2Repo
fetch_raw(sgx.from_jid).then do |(((ogm_url, fwd_time, fwd), trans_d), reg)|
sgx.with({
ogm_url: ogm_url,
- fwd: CustomerFwd.for(fwd, fwd_time),
+ fwd: CustomerFwd.for(uri: fwd, timeout: fwd_time),
transcription_enabled: !trans_d,
registered?: reg
}.compact)
end
end
+ def put_transcription_enabled(customer_id, enabled)
+ sgx = @trivial_repo.get(customer_id)
+ REDIS.setbit(
+ "catapult_settings_flags-#{sgx.from_jid}",
+ Bwmsgsv2Repo::VOICEMAIL_TRANSCRIPTION_DISABLED,
+ enabled ? 0 : 1
+ )
+ end
+
+ def put_fwd(customer_id, tel, customer_fwd)
+ sgx = @trivial_repo.get(customer_id)
+ EMPromise.all([
+ set_or_delete("catapult_fwd-#{tel}", customer_fwd.uri),
+ set_or_delete(
+ "catapult_fwd_timeout-#{sgx.from_jid}",
+ customer_fwd.timeout.to_i
+ )
+ ])
+ end
+
protected
+ def set_or_delete(k, v)
+ if v.nil?
+ REDIS.del(k)
+ else
+ REDIS.set(k, v)
+ end
+ end
+
def fetch_raw(from_jid)
registration(from_jid).then do |r|
EMPromise.all([from_redis(from_jid, r ? r.phone : nil), r])
A lib/configure_calls_form.rb => lib/configure_calls_form.rb +40 -0
@@ 0,0 1,40 @@
+# frozen_string_literal: true
+
+require_relative "form_to_h"
+
+class ConfigureCallsForm
+ using FormToH
+
+ def initialize(customer)
+ @customer = customer
+ end
+
+ def render
+ FormTemplate.render("configure_calls", customer: @customer)
+ end
+
+ def parse(form)
+ params = form.to_h
+ {}.tap do |result|
+ result[:fwd] = parse_fwd(params["fwd"]) if params.key?("fwd")
+ if params.key?("voicemail_transcription")
+ result[:transcription_enabled] =
+ ["1", "true"].include?(params["voicemail_transcription"])
+ end
+ result[:lidb_name] = params["lidb_name"] if lidb_guard(params["lidb_name"])
+ end
+ end
+
+protected
+
+ def lidb_guard(lidb_name)
+ !lidb_name.to_s.strip.empty? &&
+ !@customer.tndetails.dig(:features, :lidb)
+ end
+
+ def parse_fwd(fwd_from_form)
+ fwd_from_form.reduce(@customer.fwd) do |fwd, (var, val)|
+ fwd.with(var.to_sym => val)
+ end
+ end
+end
M lib/customer.rb => lib/customer.rb +6 -1
@@ 23,7 23,7 @@ class Customer
def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
:currency, :merchant_account, :plan_name, :auto_top_up_amount
def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
- :set_fwd, :fwd, :transcription_enabled
+ :fwd, :transcription_enabled
def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
def initialize(
@@ 83,6 83,11 @@ class Customer
stanza_to(iq, &IQ_MANAGER.method(:write)).then(&:vcard)
end
+ def tndetails
+ @tndetails ||=
+ BandwidthIris::Tn.new(telephone_number: registered?.phone).get_details
+ end
+
def ogm(from_tel=nil)
CustomerOGM.for(@sgx.ogm_url, -> { fetch_vcard_temp(from_tel) })
end
M lib/customer_fwd.rb => lib/customer_fwd.rb +35 -30
@@ 1,17 1,25 @@
# frozen_string_literal: true
+require "value_semantics/monkey_patched"
require "uri"
class CustomerFwd
- def self.for(uri, timeout)
+ def self.for(uri:, timeout:)
timeout = Timeout.new(timeout)
- return if !uri || timeout.zero?
+ return None.new(uri: uri, timeout: timeout) if !uri || timeout.zero?
+ if uri =~ /\Asip:(.*)@sip.cheogram.com\Z/
+ uri = "xmpp:#{$1.gsub(/%([0-9A-F]{2})/i) { $1.to_i(16).chr }}"
+ end
URIS.fetch(uri.split(":", 2).first.to_sym) {
raise "Unknown forward URI: #{uri}"
- }.new(uri, timeout)
+ }.new(uri: uri, timeout: timeout)
end
class Timeout
+ def self.new(s)
+ s.is_a?(self) ? s : super
+ end
+
def initialize(s)
@timeout = s.nil? || s.to_i.negative? ? 300 : s.to_i
end
@@ 25,54 33,51 @@ class CustomerFwd
end
end
- def create_call_request
+ value_semantics do
+ uri Either(String, NilClass)
+ # rubocop:disable Style/RedundantSelf
+ self.timeout Timeout, coerce: Timeout.method(:new)
+ # rubocop:enable Style/RedundantSelf
+ end
+
+ def with(new_attrs)
+ CustomerFwd.for(to_h.merge(new_attrs))
+ end
+
+ def create_call(account)
request = Bandwidth::ApiCreateCallRequest.new.tap do |cc|
cc.to = to
cc.call_timeout = timeout.to_i
+ yield cc if block_given?
end
- yield request if block_given?
- request
+ BANDWIDTH_VOICE.create_call(account, body: request).data.call_id
end
class Tel < CustomerFwd
- attr_reader :timeout
-
- def initialize(uri, timeout)
- @tel = uri.sub(/^tel:/, "")
- @timeout = timeout
- end
-
def to
- @tel
+ uri.sub(/^tel:/, "")
end
end
class SIP < CustomerFwd
- attr_reader :timeout
-
- def initialize(uri, timeout)
- @uri = uri
- @timeout = timeout
- end
-
def to
- @uri
+ uri
end
end
class XMPP < CustomerFwd
- attr_reader :timeout
-
- def initialize(uri, timeout)
- @jid = uri.sub(/^xmpp:/, "")
- @timeout = timeout
- end
-
def to
- "sip:#{ERB::Util.url_encode(@jid)}@sip.cheogram.com"
+ jid = uri.sub(/^xmpp:/, "")
+ "sip:#{ERB::Util.url_encode(jid)}@sip.cheogram.com"
end
end
+ class None < CustomerFwd
+ def create_call; end
+
+ def to; end
+ end
+
URIS = {
tel: Tel,
sip: SIP,
M lib/customer_repo.rb => lib/customer_repo.rb +27 -3
@@ 59,12 59,36 @@ class CustomerRepo
end
end
+ def put_lidb_name(customer, lidb_name)
+ BandwidthIris::Lidb.create(
+ customer_order_id: customer.customer_id,
+ lidb_tn_groups: { lidb_tn_group: {
+ telephone_numbers: [customer.registered?.phone.sub(/\A\+1/, "")],
+ subscriber_information: lidb_name,
+ use_type: "RESIDENTIAL",
+ visibility: "PUBLIC"
+ } }
+ )
+ end
+
+ def put_transcription_enabled(customer, transcription_enabled)
+ @sgx_repo.put_transcription_enabled(
+ customer.customer_id, transcription_enabled
+ )
+ end
+
+ def put_fwd(customer, customer_fwd)
+ @sgx_repo.put_fwd(
+ customer.customer_id,
+ customer.registered?.phone,
+ customer_fwd
+ )
+ end
+
protected
def new_sgx(customer_id)
- TrivialBackendSgxRepo.new.get(customer_id).with(
- registered?: false
- )
+ TrivialBackendSgxRepo.new.get(customer_id).with(registered?: false)
end
def find_legacy_customer(jid)
M lib/form_template.rb => lib/form_template.rb +24 -2
@@ 48,8 48,30 @@ class FormTemplate
@__form.instructions = s
end
- def field(**kwargs)
- @__form.fields = @__form.fields + [kwargs]
+ def validate(f, datatype: nil, **kwargs)
+ Nokogiri::XML::Builder.with(f) do |x|
+ x.validate(
+ xmlns: "http://jabber.org/protocol/xdata-validate",
+ datatype: datatype || "xs:string"
+ ) do
+ x.basic unless validation_type(x, **kwargs)
+ end
+ end
+ end
+
+ def validation_type(x, open: false, regex: nil, range: nil)
+ x.open if open
+ x.range(min: range.first, max: range.last) if range
+ x.regex(regex.source) if regex
+ open || regex || range
+ end
+
+ def field(datatype: nil, open: false, regex: nil, range: nil, **kwargs)
+ f = Blather::Stanza::X::Field.new(kwargs)
+ if datatype || open || regex || range
+ validate(f, datatype: datatype, open: open, regex: regex, range: range)
+ end
+ @__form.fields += [f]
end
def xml
M lib/registration.rb => lib/registration.rb +4 -7
@@ 445,10 445,6 @@ class Registration
}.then { |tel| Finish.new(@customer, tel).write }
end
- def cheogram_sip_addr
- "sip:#{ERB::Util.url_encode(@customer.jid)}@sip.cheogram.com"
- end
-
def raise_setup_error(e)
Command.log.error "@customer.register! failed", e
Command.finish(
@@ 459,11 455,12 @@ class Registration
end
def customer_active_tel_purchased
- @customer.register!(@tel).catch(&method(:raise_setup_error)).then { |sgx|
+ @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
EMPromise.all([
REDIS.del("pending_tel_for-#{@customer.jid}"),
- sgx.set_fwd(cheogram_sip_addr),
- sgx.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
+ Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
+ uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5 seconds / ring, 5 rings
+ ))
])
}.then do
Command.finish("Your JMP account has been activated as #{@tel}")
M sgx_jmp.rb => sgx_jmp.rb +24 -14
@@ 73,6 73,7 @@ require_relative "lib/bwmsgsv2_repo"
require_relative "lib/bandwidth_tn_order"
require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
+require_relative "lib/configure_calls_form"
require_relative "lib/command"
require_relative "lib/command_list"
require_relative "lib/customer"
@@ 453,16 454,25 @@ Command.new(
end
}.register(self).then(&CommandList.method(:register))
-# Commands that just pass through to the SGX
-{
- "configure-calls" => ["Configure Calls"]
-}.each do |node, args|
- Command.new(node, *args) {
- Command.customer.then do |customer|
- customer.stanza_from(Command.execution.iq)
+Command.new(
+ "configure calls",
+ "Configure Calls",
+ customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
+) {
+ Command.customer.then do |customer|
+ cc_form = ConfigureCallsForm.new(customer)
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << cc_form.render
+ }.then { |iq|
+ EMPromise.all(cc_form.parse(iq.form).map { |k, v|
+ Command.execution.customer_repo.public_send("put_#{k}", customer, v)
+ })
+ }.then do
+ Command.finish("Configuration saved!")
end
- }.register(self, guards: [node: node]).then(&CommandList.method(:register))
-end
+ end
+}.register(self).then(&CommandList.method(:register))
Command.new(
"ogm",
@@ 617,12 627,12 @@ Command.new(
if ["1", "true"].include?(fwd.form.field("change_fwd")&.value.to_s)
# Migrate location if needed
BandwidthIris::SipPeer.new(
- site_id: CONFIG[:bandwidth_site],
- id: CONFIG[:bandwidth_peer]
+ site_id: CONFIG[:bandwidth_site], id: CONFIG[:bandwidth_peer]
).move_tns([customer.registered?.phone])
- customer.set_fwd(sip_account.uri).then do
- Command.finish("Inbound calls will now forward to SIP.")
- end
+ Command.execution.customer_repo.put_fwd(
+ customer,
+ customer.fwd.with(uri: sip_account.uri)
+ ).then { Command.finish("Inbound calls will now forward to SIP.") }
else
Command.finish
end
A test/test_customer_fwd.rb => test/test_customer_fwd.rb +51 -0
@@ 0,0 1,51 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "customer_fwd"
+
+class Rantly
+ def jid
+ v = Blather::JID.new(Blather::JID.new(string, string).stripped.to_s)
+ guard !v.to_s.to_s.empty?
+ v
+ end
+end
+
+class CustomerFwdTest < Minitest::Test
+ property(:for_xmpp) { jid }
+ def for_xmpp(jid)
+ sip = "sip:#{ERB::Util.url_encode(jid.to_s)}@sip.cheogram.com"
+ fwd = CustomerFwd.for(uri: "xmpp:#{jid}", timeout: 10)
+ assert_kind_of CustomerFwd::XMPP, fwd
+ assert_equal sip, fwd.to
+ end
+
+ property(:for_xmpp_sip) { jid }
+ def for_xmpp_sip(jid)
+ sip = "sip:#{ERB::Util.url_encode(jid.to_s)}@sip.cheogram.com"
+ fwd = CustomerFwd.for(uri: sip, timeout: 10)
+ assert_kind_of CustomerFwd::XMPP, fwd
+ assert_equal sip, fwd.to
+ end
+
+ property(:for_tel) { "+#{string(:digit)}" }
+ def for_tel(tel)
+ fwd = CustomerFwd.for(uri: "tel:#{tel}", timeout: 10)
+ assert_kind_of CustomerFwd::Tel, fwd
+ assert_equal tel, fwd.to
+ end
+
+ property(:for_sip) { "#{string(:alnum)}@#{string(:alnum)}.example.com" }
+ def for_sip(sip)
+ fwd = CustomerFwd.for(uri: "sip:#{sip}", timeout: 10)
+ assert_kind_of CustomerFwd::SIP, fwd
+ assert_equal "sip:#{sip}", fwd.to
+ end
+
+ property(:for_bogus) { string }
+ def for_bogus(bogus)
+ assert_raises(RuntimeError) do
+ CustomerFwd.for(uri: "bogus:#{bogus}", timeout: 10)
+ end
+ end
+end
M test/test_customer_repo.rb => test/test_customer_repo.rb +80 -1
@@ 3,6 3,10 @@
require "test_helper"
require "customer_repo"
+class CustomerRepo
+ attr_reader :sgx_repo
+end
+
class CustomerRepoTest < Minitest::Test
FAKE_REDIS = FakeRedis.new(
# sgx-jmp customer
@@ 51,7 55,13 @@ class CustomerRepoTest < Minitest::Test
db: FAKE_DB,
braintree: Minitest::Mock.new
)
- CustomerRepo.new(redis: redis, db: db, braintree: braintree)
+ sgx_repo = Minitest::Mock.new(TrivialBackendSgxRepo.new)
+ CustomerRepo.new(
+ redis: redis,
+ db: db,
+ braintree: braintree,
+ sgx_repo: sgx_repo
+ )
end
def setup
@@ 153,4 163,73 @@ class CustomerRepoTest < Minitest::Test
assert_mock redis
end
em :test_create
+
+ def test_put_lidb_name
+ post = stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//lidbs"
+ ).with(body: {
+ CustomerOrderId: "test",
+ LidbTnGroups: {
+ LidbTnGroup: {
+ TelephoneNumbers: "5556667777",
+ SubscriberInformation: "Hank",
+ UseType: "RESIDENTIAL",
+ Visibility: "PUBLIC"
+ }
+ }
+ }.to_xml(root: "LidbOrder", indent: 0)).to_return(
+ status: 201,
+ headers: { location: "/boop/123" }
+ )
+
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//lidbs/123"
+ )
+
+ @repo.put_lidb_name(
+ Customer.new(
+ "test",
+ "test@exmple.com",
+ sgx: OpenStruct.new(registered?: OpenStruct.new(phone: "+15556667777"))
+ ),
+ "Hank"
+ )
+
+ assert_requested post
+ end
+ em :test_put_lidb_name
+
+ def test_put_transcription_enabled
+ @repo.sgx_repo.expect(
+ :put_transcription_enabled,
+ EMPromise.resolve(nil),
+ ["test", true]
+ )
+ @repo.put_transcription_enabled(
+ Customer.new("test", "test@exmple.com"),
+ true
+ )
+ assert_mock @repo.sgx_repo
+ end
+ em :test_put_transcription_enabled
+
+ def test_put_fwd
+ @repo.sgx_repo.expect(
+ :put_fwd,
+ EMPromise.resolve(nil),
+ ["test", "+15556667777", :fwd]
+ )
+ @repo.put_fwd(
+ Customer.new(
+ "test",
+ "test@exmple.com",
+ sgx: OpenStruct.new(registered?: OpenStruct.new(phone: "+15556667777"))
+ ),
+ :fwd
+ )
+ assert_mock @repo.sgx_repo
+ end
+ em :test_put_fwd
end
M test/test_form_template.rb => test/test_form_template.rb +68 -0
@@ 46,6 46,74 @@ class FormTemplateTest < Minitest::Test
assert_equal "INSTRUCTIONS", form.instructions
end
+ def test_form_validate_basic
+ template = FormTemplate.new(<<~TEMPLATE)
+ form!
+ field(var: "thevar", label: "thelabel", datatype: "xs:integer")
+ TEMPLATE
+ form = template.render
+ assert_equal 1, form.fields.length
+ assert_equal "thevar", form.fields[0].var
+ assert_equal "thelabel", form.fields[0].label
+ validate = form.fields[0].find(
+ "ns:validate",
+ ns: "http://jabber.org/protocol/xdata-validate"
+ ).first
+ assert_equal "xs:integer", validate[:datatype]
+ assert_equal "basic", validate.children.first.name
+ end
+
+ def test_form_validate_open
+ template = FormTemplate.new(<<~TEMPLATE)
+ form!
+ field(var: "thevar", label: "thelabel", open: true)
+ TEMPLATE
+ form = template.render
+ assert_equal 1, form.fields.length
+ assert_equal "thevar", form.fields[0].var
+ assert_equal "thelabel", form.fields[0].label
+ validate = form.fields[0].find(
+ "ns:validate",
+ ns: "http://jabber.org/protocol/xdata-validate"
+ ).first
+ assert_equal ["open"], validate.children.map(&:name)
+ end
+
+ def test_form_validate_regex
+ template = FormTemplate.new(<<~TEMPLATE)
+ form!
+ field(var: "thevar", label: "thelabel", regex: /[A-Z]/)
+ TEMPLATE
+ form = template.render
+ assert_equal 1, form.fields.length
+ assert_equal "thevar", form.fields[0].var
+ assert_equal "thelabel", form.fields[0].label
+ validate = form.fields[0].find(
+ "ns:validate",
+ ns: "http://jabber.org/protocol/xdata-validate"
+ ).first
+ assert_equal ["regex"], validate.children.map(&:name)
+ assert_equal "[A-Z]", validate.children.first.content
+ end
+
+ def test_form_validate_range
+ template = FormTemplate.new(<<~TEMPLATE)
+ form!
+ field(var: "thevar", label: "thelabel", range: (10..22))
+ TEMPLATE
+ form = template.render
+ assert_equal 1, form.fields.length
+ assert_equal "thevar", form.fields[0].var
+ assert_equal "thelabel", form.fields[0].label
+ validate = form.fields[0].find(
+ "ns:validate",
+ ns: "http://jabber.org/protocol/xdata-validate"
+ ).first
+ assert_equal ["range"], validate.children.map(&:name)
+ assert_equal "10", validate.children.first[:min]
+ assert_equal "22", validate.children.first[:max]
+ end
+
def test_no_type
template = FormTemplate.new(<<~TEMPLATE)
title "TITLE"
M test/test_registration.rb => test/test_registration.rb +5 -5
@@ 517,7 517,7 @@ class RegistrationTest < Minitest::Test
Command::COMMAND_MANAGER = Minitest::Mock.new
Registration::Finish::TEL_SELECTIONS = FakeTelSelections.new
Registration::Finish::REDIS = Minitest::Mock.new
- BackendSgx::REDIS = Minitest::Mock.new
+ Bwmsgsv2Repo::REDIS = Minitest::Mock.new
def setup
@sgx = Minitest::Mock.new(TrivialBackendSgxRepo.new.get("test"))
@@ 568,15 568,15 @@ class RegistrationTest < Minitest::Test
nil,
["pending_tel_for-test@example.net"]
)
- BackendSgx::REDIS.expect(
+ Bwmsgsv2Repo::REDIS.expect(
:set,
nil,
[
"catapult_fwd-+15555550000",
- "sip:test%40example.net@sip.cheogram.com"
+ "xmpp:test@example.net"
]
)
- BackendSgx::REDIS.expect(
+ Bwmsgsv2Repo::REDIS.expect(
:set,
nil,
["catapult_fwd_timeout-customer_test@component", 25]
@@ 608,7 608,7 @@ class RegistrationTest < Minitest::Test
assert_requested create_order
assert_mock @sgx
assert_mock Registration::Finish::REDIS
- assert_mock BackendSgx::REDIS
+ assert_mock Bwmsgsv2Repo::REDIS
assert_mock blather
end
em :test_write
M web.rb => web.rb +9 -10
@@ 257,17 257,16 @@ class Web < Roda
CustomerRepo.new(
sgx_repo: Bwmsgsv2Repo.new
).find_by_tel(params["to"]).then(&:fwd).then do |fwd|
- if fwd
+ call = fwd.create_call(CONFIG[:creds][:account]) do |cc|
true_inbound_call[pseudo_call_id] = params["callId"]
- outbound_transfers[pseudo_call_id] = BANDWIDTH_VOICE.create_call(
- CONFIG[:creds][:account],
- body: fwd.create_call_request do |cc|
- cc.from = params["from"]
- cc.application_id = params["applicationId"]
- cc.answer_url = url inbound_calls_path(nil)
- cc.disconnect_url = url inbound_calls_path(:transfer_complete)
- end
- ).data.call_id
+ cc.from = params["from"]
+ cc.application_id = params["applicationId"]
+ cc.answer_url = url inbound_calls_path(nil)
+ cc.disconnect_url = url inbound_calls_path(:transfer_complete)
+ end
+
+ if call
+ outbound_transfers[pseudo_call_id] = call
render :pause, locals: { duration: 300 }
else
render :redirect, locals: { to: inbound_calls_path(:voicemail) }