Merge branch 'configure-calls-v2'

Stephen Paul Weber created

* configure-calls-v2:
  New configure calls command
  Move more persistence into the repo layer
  Easy DSL for adding XEP-0122 validation to fields
  CustomerFwd uses ValueSemantics, translates old XMPP-SIP URI

Change summary

forms/configure_calls.rb    | 48 +++++++++++++++++++++++
lib/backend_sgx.rb          |  8 ---
lib/bwmsgsv2_repo.rb        | 30 +++++++++++++
lib/configure_calls_form.rb | 40 +++++++++++++++++++
lib/customer.rb             |  7 ++
lib/customer_fwd.rb         | 65 ++++++++++++++++--------------
lib/customer_repo.rb        | 30 ++++++++++++-
lib/form_template.rb        | 26 +++++++++++
lib/registration.rb         | 11 +---
sgx_jmp.rb                  | 38 +++++++++++------
test/test_customer_fwd.rb   | 51 ++++++++++++++++++++++++
test/test_customer_repo.rb  | 81 ++++++++++++++++++++++++++++++++++++++
test/test_form_template.rb  | 68 ++++++++++++++++++++++++++++++++
test/test_registration.rb   | 10 ++--
web.rb                      | 19 ++++----
15 files changed, 450 insertions(+), 82 deletions(-)

Detailed changes

forms/configure_calls.rb 🔗

@@ -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

lib/backend_sgx.rb 🔗

@@ -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

lib/bwmsgsv2_repo.rb 🔗

@@ -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])

lib/configure_calls_form.rb 🔗

@@ -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

lib/customer.rb 🔗

@@ -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

lib/customer_fwd.rb 🔗

@@ -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,

lib/customer_repo.rb 🔗

@@ -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)

lib/form_template.rb 🔗

@@ -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

lib/registration.rb 🔗

@@ -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}")

sgx_jmp.rb 🔗

@@ -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

test/test_customer_fwd.rb 🔗

@@ -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

test/test_customer_repo.rb 🔗

@@ -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

test/test_form_template.rb 🔗

@@ -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"

test/test_registration.rb 🔗

@@ -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

web.rb 🔗

@@ -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) }