diff --git a/lib/call_attempt.rb b/lib/call_attempt.rb index 53301146b2120049582c0111d696097ecb10a916..53156b50df6b7192833f6dbc1436fe2599020951 100644 --- a/lib/call_attempt.rb +++ b/lib/call_attempt.rb @@ -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 diff --git a/lib/call_attempt_repo.rb b/lib/call_attempt_repo.rb index 1bf751a1cef92dec275e6f81620b32a550464976..dc9a6e0a51164f1a5a99132fb6ca44302f8074d5 100644 --- a/lib/call_attempt_repo.rb +++ b/lib/call_attempt_repo.rb @@ -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 diff --git a/lib/customer_fwd.rb b/lib/customer_fwd.rb index b723097bcc85b27a50374ce160c3b694cd62c34d..4a6f4350ed7df0ed3af030cf91423b7386245f21 100644 --- a/lib/customer_fwd.rb +++ b/lib/customer_fwd.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "bandwidth" require "value_semantics/monkey_patched" require "uri" diff --git a/test/test_helper.rb b/test/test_helper.rb index 53169be94161977ad5c8945c302cfac9c2378323..e105fe448c6e8aace27a2ae60e67eaf508ee980f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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 diff --git a/test/test_web.rb b/test/test_web.rb index b9fbb0c894bb631af23f83fe5badefb6ad677f48..8fecd8f8533e028784ce8c361e7d62308e415a7e 100644 --- a/test/test_web.rb +++ b/test/test_web.rb @@ -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( + "" \ + "" \ + "", + 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( + "" \ + "" \ + "", + 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( + "" \ + "connectedacall" \ + "", + 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( + "" \ + "This call will take you over " \ + "your configured monthly overage limit." \ + "Change your limit in your account settings or press 1 to accept the " \ + "charges. You can hang up to send the caller to voicemail." \ + "", + 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( + "" \ + "connectedacall" \ + "", + 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( :<<, diff --git a/views/inbound/at_limit.slim b/views/inbound/at_limit.slim new file mode 100644 index 0000000000000000000000000000000000000000..44b1d24fe194701ae38ed7cbe4352e484a6ae36c --- /dev/null +++ b/views/inbound/at_limit.slim @@ -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. diff --git a/views/bridge.slim b/views/inbound/connect.slim similarity index 71% rename from views/bridge.slim rename to views/inbound/connect.slim index a97957cebd1a5d2c2bd916536578cb2088b51cb5..9f8537c79c72a660f6bfbda17776a0f5c94d1d7e 100644 --- a/views/bridge.slim +++ b/views/inbound/connect.slim @@ -1,3 +1,4 @@ doctype xml Response + Tag connected Bridge= call_id diff --git a/views/inbound/no_balance.slim b/views/inbound/no_balance.slim new file mode 100644 index 0000000000000000000000000000000000000000..68e146def972c111d1df3a816975dd7482678fb4 --- /dev/null +++ b/views/inbound/no_balance.slim @@ -0,0 +1,3 @@ +doctype xml +Response + Hangup / diff --git a/views/inbound/unsupported.slim b/views/inbound/unsupported.slim new file mode 100644 index 0000000000000000000000000000000000000000..68e146def972c111d1df3a816975dd7482678fb4 --- /dev/null +++ b/views/inbound/unsupported.slim @@ -0,0 +1,3 @@ +doctype xml +Response + Hangup / diff --git a/views/forward.slim b/views/outbound/connect.slim similarity index 100% rename from views/forward.slim rename to views/outbound/connect.slim diff --git a/views/ring.slim b/views/ring.slim index 825dcb7fd8b1833bc0c6470f0939d82ccfb8b479..76a69c821bb81d0718a18843d5009bf51284197e 100644 --- a/views/ring.slim +++ b/views/ring.slim @@ -1,3 +1,3 @@ doctype xml Response - Ring duration=duration answerCall="false" + Ring duration=duration answerCall="false" / diff --git a/web.rb b/web.rb index 4b85d3ec62a9a50e9c93eb65806e3c5447b3f26d..74482b8e6a4f04c5ccec5059a4ae0c2025081192 100644 --- a/web.rb +++ b/web.rb @@ -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