From 3e7035106f3121e8fdf22d0f20ecc835bc620815 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 31 Jan 2022 21:16:09 -0500 Subject: [PATCH] Also supports limits on inbound calls --- lib/call_attempt.rb | 48 ++++- lib/call_attempt_repo.rb | 6 +- lib/customer_fwd.rb | 1 + test/test_helper.rb | 14 ++ test/test_web.rb | 203 +++++++++++++++++- views/inbound/at_limit.slim | 5 + views/{bridge.slim => inbound/connect.slim} | 1 + views/inbound/no_balance.slim | 3 + views/inbound/unsupported.slim | 3 + views/{forward.slim => outbound/connect.slim} | 0 views/ring.slim | 2 +- web.rb | 27 ++- 12 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 views/inbound/at_limit.slim rename views/{bridge.slim => inbound/connect.slim} (71%) create mode 100644 views/inbound/no_balance.slim create mode 100644 views/inbound/unsupported.slim rename views/{forward.slim => outbound/connect.slim} (100%) diff --git a/lib/call_attempt.rb b/lib/call_attempt.rb index 5330114..53156b5 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 1bf751a..dc9a6e0 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 b723097..4a6f435 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 53169be..e105fe4 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 b9fbb0c..8fecd8f 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 0000000..44b1d24 --- /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 a97957c..9f8537c 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 0000000..68e146d --- /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 0000000..68e146d --- /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 825dcb7..76a69c8 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 4b85d3e..74482b8 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 -- 2.38.5