M lib/call_attempt.rb => lib/call_attempt.rb +37 -11
@@ 8,54 8,80 @@ class CallAttempt
"cad_beta_unlimited-v20210223" => 1.1
}.freeze
- def self.for(customer, other_tel, rate, usage, digits)
+ def self.for(customer, other_tel, rate, usage, direction:, **kwargs)
included_credit = [customer.minute_limit.to_d - usage, 0].max
if !rate || rate >= EXPENSIVE_ROUTE.fetch(customer.plan_name, 0.1)
- Unsupported.new
+ Unsupported.new(direction: direction)
elsif included_credit + customer.balance < rate * 10
- NoBalance.new(balance: customer.balance)
+ NoBalance.new(balance: customer.balance, direction: direction)
else
- for_ask_or_go(customer, other_tel, rate, usage, digits)
+ for_ask_or_go(
+ customer, other_tel, rate, usage, direction: direction, **kwargs
+ )
end
end
- def self.for_ask_or_go(customer, other_tel, rate, usage, digits)
+ def self.for_ask_or_go(customer, otel, rate, usage, digits: nil, **kwargs)
can_use = customer.minute_limit.to_d + customer.monthly_overage_limit
if digits != "1" && can_use - usage < rate * 10
- AtLimit.new
+ AtLimit.new(**kwargs.slice(:direction, :call_id))
else
- new(from: customer.registered?.phone, to: other_tel)
+ new(from: customer.registered?.phone, to: otel, **kwargs)
end
end
value_semantics do
from(/\A\+\d+\Z/)
to(/\A\+\d+\Z/)
+ call_id String
+ direction Either(:inbound, :outbound)
end
def to_render
- [:forward, { locals: to_h }]
+ ["#{direction}/connect", { locals: to_h }]
+ end
+
+ def create_call(fwd, *args, &block)
+ fwd.create_call(*args, &block)
end
class Unsupported
+ value_semantics do
+ direction Either(:inbound, :outbound)
+ end
+
def to_render
- ["outbound/unsupported"]
+ ["#{direction}/unsupported"]
end
+
+ def create_call(*); end
end
class NoBalance
value_semantics do
balance Numeric
+ direction Either(:inbound, :outbound)
end
def to_render
- ["outbound/no_balance", { locals: to_h }]
+ ["#{direction}/no_balance", { locals: to_h }]
end
+
+ def create_call(*); end
end
class AtLimit
+ value_semantics do
+ call_id String
+ direction Either(:inbound, :outbound)
+ end
+
def to_render
- ["outbound/at_limit"]
+ ["#{direction}/at_limit", { locals: to_h }]
+ end
+
+ def create_call(fwd, *args, &block)
+ fwd.create_call(*args, &block)
end
end
end
M lib/call_attempt_repo.rb => lib/call_attempt_repo.rb +4 -2
@@ 9,12 9,14 @@ class CallAttemptRepo
db Anything(), default: LazyObject.new { DB }
end
- def find(customer, other_tel, digits=nil, direction=:outbound)
+ def find(customer, other_tel, direction: :outbound, **kwargs)
EMPromise.all([
find_rate(customer.plan_name, other_tel, direction),
find_usage(customer.customer_id)
]).then do |(rate, usage)|
- CallAttempt.for(customer, other_tel, rate, usage, digits)
+ CallAttempt.for(
+ customer, other_tel, rate, usage, direction: direction, **kwargs
+ )
end
end
M lib/customer_fwd.rb => lib/customer_fwd.rb +1 -0
@@ 1,5 1,6 @@
# frozen_string_literal: true
+require "bandwidth"
require "value_semantics/monkey_patched"
require "uri"
M test/test_helper.rb => test/test_helper.rb +14 -0
@@ 205,6 205,20 @@ class FakeDB
end
end
+class FakeLog
+ def initialize
+ @logs = []
+ end
+
+ def respond_to_missing?(*)
+ true
+ end
+
+ def method_missing(*args)
+ @logs << args
+ end
+end
+
class FakeIBRRepo
def initialize(registrations={})
@registrations = registrations
M test/test_web.rb => test/test_web.rb +196 -7
@@ 5,6 5,8 @@ require "test_helper"
require_relative "../web"
Customer::BLATHER = Minitest::Mock.new
+CustomerFwd::BANDWIDTH_VOICE = Minitest::Mock.new
+Web::BANDWIDTH_VOICE = Minitest::Mock.new
class WebTest < Minitest::Test
include Rack::Test::Methods
@@ 15,7 17,9 @@ class WebTest < Minitest::Test
"jmp_customer_jid-customerid" => "customer@example.com",
"catapult_jid-+15551234567" => "customer_customerid@component",
"jmp_customer_jid-customerid_low" => "customer@example.com",
- "jmp_customer_jid-customerid_limit" => "customer@example.com"
+ "catapult_jid-+15551234560" => "customer_customerid_low@component",
+ "jmp_customer_jid-customerid_limit" => "customer@example.com",
+ "catapult_jid-+15551234561" => "customer_customerid_limit@component"
),
db: FakeDB.new(
["customerid"] => [{
@@ 35,7 39,14 @@ class WebTest < Minitest::Test
}]
),
sgx_repo: Bwmsgsv2Repo.new(
- redis: FakeRedis.new,
+ redis: FakeRedis.new(
+ "catapult_fwd-+15551234567" => "xmpp:customer@example.com",
+ "catapult_fwd_timeout-customer_customerid@component" => "30",
+ "catapult_fwd-+15551234560" => "xmpp:customer@example.com",
+ "catapult_fwd_timeout-customer_customerid_low@component" => "30",
+ "catapult_fwd-+15551234561" => "xmpp:customer@example.com",
+ "catapult_fwd_timeout-customer_customerid_limit@component" => "30"
+ ),
ibr_repo: FakeIBRRepo.new(
"sgx" => {
"customer_customerid@component" => IBR.new.tap do |ibr|
@@ 51,17 62,24 @@ class WebTest < Minitest::Test
Web.opts[:call_attempt_repo] = CallAttemptRepo.new(
db: FakeDB.new(
["test_usd", "+15557654321", :outbound] => [{ "rate" => 0.01 }],
+ ["test_usd", "+15557654321", :inbound] => [{ "rate" => 0.01 }],
["customerid_limit"] => [{ "a" => -1000 }],
["customerid_low"] => [{ "a" => -1000 }]
)
)
+ Web.opts[:common_logger] = FakeLog.new
+ Web.instance_variable_set(:@outbound_transfers, { "bcall" => "oocall" })
Web.app
end
def test_outbound_forwards
post(
"/outbound/calls",
- { from: "customerid", to: "+15557654321" }.to_json,
+ {
+ from: "customerid",
+ to: "+15557654321",
+ callId: "acall"
+ }.to_json,
{ "CONTENT_TYPE" => "application/json" }
)
@@ 78,7 96,11 @@ class WebTest < Minitest::Test
def test_outbound_low_balance
post(
"/outbound/calls",
- { from: "customerid_low", to: "+15557654321" }.to_json,
+ {
+ from: "customerid_low",
+ to: "+15557654321",
+ callId: "acall"
+ }.to_json,
{ "CONTENT_TYPE" => "application/json" }
)
@@ 95,7 117,11 @@ class WebTest < Minitest::Test
def test_outbound_unsupported
post(
"/outbound/calls",
- { from: "customerid", to: "+95557654321" }.to_json,
+ {
+ from: "customerid_limit",
+ to: "+95557654321",
+ callId: "acall"
+ }.to_json,
{ "CONTENT_TYPE" => "application/json" }
)
@@ 112,7 138,11 @@ class WebTest < Minitest::Test
def test_outbound_atlimit
post(
"/outbound/calls",
- { from: "customerid_limit", to: "+15557654321" }.to_json,
+ {
+ from: "customerid_limit",
+ to: "+15557654321",
+ callId: "acall"
+ }.to_json,
{ "CONTENT_TYPE" => "application/json" }
)
@@ 132,7 162,12 @@ class WebTest < Minitest::Test
def test_outbound_atlimit_digits
post(
"/outbound/calls",
- { from: "customerid_limit", to: "+15557654321", digits: "1" }.to_json,
+ {
+ from: "customerid_limit",
+ to: "+15557654321",
+ callId: "acall",
+ digits: "1"
+ }.to_json,
{ "CONTENT_TYPE" => "application/json" }
)
@@ 146,6 181,160 @@ class WebTest < Minitest::Test
end
em :test_outbound_atlimit_digits
+ def test_inbound
+ CustomerFwd::BANDWIDTH_VOICE.expect(
+ :create_call,
+ OpenStruct.new(data: OpenStruct.new(call_id: "ocall")),
+ [
+ "test_bw_account",
+ Matching.new { |arg| assert_equal [:body], arg.keys }
+ ]
+ )
+
+ post(
+ "/inbound/calls",
+ {
+ from: "+15557654321",
+ to: "+15551234567",
+ callId: "acall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
+ "<Ring answerCall=\"false\" duration=\"300\" />" \
+ "</Response>",
+ last_response.body
+ )
+ assert_mock CustomerFwd::BANDWIDTH_VOICE
+ end
+ em :test_inbound
+
+ def test_inbound_low
+ post(
+ "/inbound/calls",
+ {
+ from: "+15557654321",
+ to: "+15551234560",
+ callId: "acall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
+ "<Redirect redirectUrl=\"/inbound/calls/acall/voicemail\" />" \
+ "</Response>",
+ last_response.body
+ )
+ assert_mock CustomerFwd::BANDWIDTH_VOICE
+ end
+ em :test_inbound_low
+
+ def test_inbound_leg2
+ post(
+ "/inbound/calls/acall",
+ {
+ from: "+15557654321",
+ to: "+15551234567",
+ callId: "ocall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
+ "<Tag>connected</Tag><Bridge>acall</Bridge>" \
+ "</Response>",
+ last_response.body
+ )
+ end
+ em :test_inbound_leg2
+
+ def test_inbound_limit_leg2
+ post(
+ "/inbound/calls/acall",
+ {
+ from: "+15557654321",
+ to: "+15551234561",
+ callId: "ocall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
+ "<Gather gatherUrl=\"\/inbound/calls/acall\" maxDigits=\"1\" " \
+ "repeatCount=\"3\"><SpeakSentence>This call will take you over " \
+ "your configured monthly overage limit.</SpeakSentence><SpeakSentence>" \
+ "Change your limit in your account settings or press 1 to accept the " \
+ "charges. You can hang up to send the caller to voicemail." \
+ "</SpeakSentence></Gather></Response>",
+ last_response.body
+ )
+ end
+ em :test_inbound_limit_leg2
+
+ def test_inbound_limit_digits_leg2
+ post(
+ "/inbound/calls/acall",
+ {
+ from: "+15557654321",
+ to: "+15551234561",
+ callId: "ocall",
+ digits: "1"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
+ "<Tag>connected</Tag><Bridge>acall</Bridge>" \
+ "</Response>",
+ last_response.body
+ )
+ end
+ em :test_inbound_limit_digits_leg2
+
+ def test_inbound_limit_hangup
+ Web::BANDWIDTH_VOICE.expect(
+ :modify_call,
+ nil,
+ [
+ "test_bw_account",
+ "bcall",
+ Matching.new do |arg|
+ assert_equal [:body], arg.keys
+ assert_equal(
+ "http://example.org/inbound/calls/oocall/voicemail",
+ arg[:body].redirect_url
+ )
+ end
+ ]
+ )
+
+ post(
+ "/inbound/calls/bcall/transfer_complete",
+ {
+ from: "+15557654321",
+ to: "+15551234561",
+ callId: "oocall",
+ cause: "hangup"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_mock Web::BANDWIDTH_VOICE
+ end
+ em :test_inbound_limit_hangup
+
def test_voicemail
Customer::BLATHER.expect(
:<<,
A views/inbound/at_limit.slim => views/inbound/at_limit.slim +5 -0
@@ 0,0 1,5 @@
+doctype xml
+Response
+ Gather gatherUrl="/inbound/calls/#{call_id}" maxDigits="1" repeatCount="3"
+ SpeakSentence This call will take you over your configured monthly overage limit.
+ SpeakSentence Change your limit in your account settings or press 1 to accept the charges. You can hang up to send the caller to voicemail.
R views/bridge.slim => views/inbound/connect.slim +1 -0
@@ 1,3 1,4 @@
doctype xml
Response
+ Tag connected
Bridge= call_id
A views/inbound/no_balance.slim => views/inbound/no_balance.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Hangup /
A views/inbound/unsupported.slim => views/inbound/unsupported.slim +3 -0
@@ 0,0 1,3 @@
+doctype xml
+Response
+ Hangup /
R views/forward.slim => views/outbound/connect.slim +0 -0
M views/ring.slim => views/ring.slim +1 -1
@@ 1,3 1,3 @@
doctype xml
Response
- Ring duration=duration answerCall="false"
+ Ring duration=duration answerCall="false" /
M web.rb => web.rb +22 -5
@@ 172,7 172,7 @@ class Web < Roda
r.on :call_id do |call_id|
r.post "transfer_complete" do
outbound_leg = outbound_transfers.delete(call_id)
- if params["cause"] == "hangup"
+ if params["cause"] == "hangup" && params["tag"] == "connected"
log.info "Normal hangup, now end #{call_id}", loggable_params
modify_call(call_id) { |call| call.state = "completed" }
elsif !outbound_leg
@@ 244,15 244,31 @@ class Web < Roda
end
r.post do
- render :bridge, locals: { call_id: call_id }
+ customer_repo.find_by_tel(params["to"]).then do |customer|
+ call_attempt_repo.find(
+ customer,
+ params["from"],
+ call_id: call_id,
+ digits: params["digits"],
+ direction: :inbound
+ ).then { |ca| render(*ca.to_render) }
+ end
end
end
r.post do
customer_repo(
sgx_repo: Bwmsgsv2Repo.new
- ).find_by_tel(params["to"]).then(&:fwd).then do |fwd|
- call = fwd.create_call(CONFIG[:creds][:account]) { |cc|
+ ).find_by_tel(params["to"]).then { |customer|
+ EMPromise.all([
+ customer.fwd,
+ call_attempt_repo.find(
+ customer, params["from"],
+ call_id: params["callId"], direction: :inbound
+ )
+ ])
+ }.then do |(fwd, ca)|
+ call = ca.create_call(fwd, CONFIG[:creds][:account]) { |cc|
cc.from = params["from"]
cc.application_id = params["applicationId"]
cc.answer_url = url inbound_calls_path(nil)
@@ 288,7 304,8 @@ class Web < Roda
call_attempt_repo.find(
c,
params["to"],
- params["digits"]
+ call_id: params["callId"],
+ digits: params["digits"]
).then { |ca| render(*ca.to_render) }
end
end