Admin command to cancel customer

Stephen Paul Weber created

Notify customer
Deregister from SGX
Deregister from Cheogram
Disconnect from Bandwidth
	If on special list, move intead of disconnect

Change summary

Gemfile                    |   2 
config-schema.dhall        |   2 
config.dhall.sample        |   2 
forms/admin_menu.rb        |   3 
lib/admin_command.rb       |  13 ++++
lib/backend_sgx.rb         |   7 ++
lib/bandwidth_tn_repo.rb   |  22 +++++++
lib/customer.rb            |   3 
lib/customer_repo.rb       |   5 +
lib/ibr.rb                 |  11 +++
test/test_admin_command.rb | 115 ++++++++++++++++++++++++++++++++++++++++
test/test_helper.rb        |   2 
12 files changed, 184 insertions(+), 3 deletions(-)

Detailed changes

Gemfile 🔗

@@ -20,7 +20,7 @@ gem "multihashes"
 gem "ougai"
 gem "relative_time"
 gem "roda"
-gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "sip_credential"
+gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "tn_move"
 gem "sentry-ruby", "<= 4.3.1"
 gem "slim"
 gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"

config-schema.dhall 🔗

@@ -19,6 +19,8 @@
 , electrum_notify_url :
     forall (address : Text) -> forall (customer_id : Text) -> Text
 , interac : Text
+, keep_area_codes : List Text
+, keep_area_codes_in : { account : Text, site : Text }
 , notify_admin : Text
 , notify_from : Text
 , ogm_path : Text

config.dhall.sample 🔗

@@ -77,6 +77,8 @@ in
 	notify_from = "+15551234567@example.net",
 	admins = ["test\\40example.com@example.net"],
 	unbilled_targets = ["+14169938000"],
+	keep_area_codes = ["555"],
+	keep_area_codes_in = { account = "", site = "" },
 	upstream_domain = "example.net",
 	approved_domains = toMap { `example.com` = Some "customer_id" }
 }

forms/admin_menu.rb 🔗

@@ -10,6 +10,7 @@ field(
 	options: [
 		{ value: "info", label: "Customer Info" },
 		{ value: "financial", label: "Customer Billing Information" },
-		{ value: "bill_plan", label: "Bill Customer" }
+		{ value: "bill_plan", label: "Bill Customer" },
+		{ value: "cancel_account", label: "Cancel Customer" }
 	]
 )

lib/admin_command.rb 🔗

@@ -75,6 +75,19 @@ class AdminCommand
 		BillPlanCommand.for(@target_customer).call
 	end
 
+	def action_cancel_account
+		m = Blather::Stanza::Message.new
+		m.from = CONFIG[:notify_from]
+		m.body = "Your JMP account has been cancelled."
+		@target_customer.stanza_to(m).then {
+			EMPromise.all([
+				@target_customer.stanza_to(IBR.new(:set).tap(&:remove!)),
+				@target_customer.deregister!,
+				@customer_repo.disconnect_tel(@target_customer)
+			])
+		}
+	end
+
 	def pay_methods(financial_info)
 		reply(FormTemplate.render(
 			"admin_payment_methods",

lib/backend_sgx.rb 🔗

@@ -27,6 +27,13 @@ class BackendSgx
 		IQ_MANAGER.write(ibr)
 	end
 
+	def deregister!
+		ibr = IBR.new(:set, @jid)
+		ibr.from = from_jid
+		ibr.remove!
+		IQ_MANAGER.write(ibr)
+	end
+
 	def stanza(s)
 		s.dup.tap do |stanza|
 			stanza.to = stanza.to.with(domain: jid.domain)

lib/bandwidth_tn_repo.rb 🔗

@@ -3,6 +3,15 @@
 require "ruby-bandwidth-iris"
 
 class BandwidthTnRepo
+	def initialize
+		@move_client =
+			BandwidthIris::Client.new(
+				account_id: CONFIG[:keep_area_codes_in][:account],
+				username: CONFIG[:creds][:username],
+				password: CONFIG[:creds][:password]
+			)
+	end
+
 	def find(tel)
 		BandwidthIris::Tn.new(telephone_number: tel).get_details
 	end
@@ -18,4 +27,17 @@ class BandwidthTnRepo
 	rescue BandwidthIris::Errors::GenericError
 		raise "Could not set CNAM, please contact support"
 	end
+
+	def disconnect(tel, order_name)
+		tn = tel.sub(/\A\+1/, "")
+		if CONFIG[:keep_area_codes].find { |area| tn.start_with?(area) }
+			BandwidthIris::Tn.new({ telephone_number: tn }, @move_client).move(
+				site_id: CONFIG[:keep_area_codes_in][:site],
+				customer_order_id: order_name,
+				source_account_id: CONFIG[:creds][:account]
+			)
+		else
+			BandwidthIris::Disconnect.create(order_name, tn)
+		end
+	end
 end

lib/customer.rb 🔗

@@ -26,7 +26,7 @@ class Customer
 	               :currency, :merchant_account, :plan_name, :minute_limit,
 	               :message_limit, :auto_top_up_amount, :monthly_overage_limit,
 	               :monthly_price, :save_plan!
-	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
+	def_delegators :@sgx, :deregister!, :register!, :registered?, :set_ogm_url,
 	               :fwd, :transcription_enabled
 	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
 	def_delegators :@financials, :payment_methods, :btc_addresses,
@@ -85,6 +85,7 @@ class Customer
 	def stanza_to(stanza)
 		stanza = stanza.dup
 		stanza.to = jid.with(resource: stanza.to&.resource)
+		stanza.from ||= Blather::JID.new("")
 		stanza.from = stanza.from.with(domain: CONFIG[:component][:jid])
 		block_given? ? yield(stanza) : (BLATHER << stanza)
 	end

lib/customer_repo.rb 🔗

@@ -107,6 +107,11 @@ class CustomerRepo
 		end
 	end
 
+	def disconnect_tel(customer)
+		tel = customer.registered?.phone
+		@bandwidth_tn_repo.disconnect(tel, customer.customer_id)
+	end
+
 	def put_lidb_name(customer, lidb_name)
 		@bandwidth_tn_repo.put_lidb_name(customer.registered?.phone, lidb_name)
 	end

lib/ibr.rb 🔗

@@ -16,6 +16,17 @@ class IBR < Blather::Stanza::Iq::Query
 		!!query.at_xpath("./ns:registered", ns: self.class.registered_ns)
 	end
 
+	def remove!
+		query.children.remove
+		node = Nokogiri::XML::Node.new("remove", document)
+		node.default_namespace = self.class.registered_ns
+		query << node
+	end
+
+	def remove?
+		!!query.at_xpath("./ns:remove", ns: self.class.registered_ns)
+	end
+
 	[
 		"instructions",
 		"username",

test/test_admin_command.rb 🔗

@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require "admin_command"
+
+BackendSgx::IQ_MANAGER = Minitest::Mock.new
+Customer::BLATHER = Minitest::Mock.new
+
+class AdminCommandTest < Minitest::Test
+	def admin_command(tel="+15556667777")
+		sgx = Minitest::Mock.new(OpenStruct.new(
+			registered?: OpenStruct.new(phone: tel)
+		))
+		[sgx, AdminCommand.new(customer(sgx: sgx), CustomerRepo.new)]
+	end
+
+	def test_action_cancel_account
+		sgx, admin = admin_command
+
+		Customer::BLATHER.expect(
+			:<<,
+			EMPromise.resolve(nil),
+			[
+				Matching.new do |m|
+					assert_equal "Your JMP account has been cancelled.", m.body
+					assert_equal "test@example.net", m.to.to_s
+					assert_equal "notify_from@component", m.from.to_s
+				end
+			]
+		)
+
+		Customer::BLATHER.expect(
+			:<<,
+			EMPromise.resolve(nil),
+			[
+				Matching.new do |iq|
+					assert iq.remove?
+					assert_equal "test@example.net", iq.to.to_s
+					assert_equal "component", iq.from.to_s
+				end
+			]
+		)
+
+		sgx.expect(:deregister!, EMPromise.resolve(nil))
+
+		stub_request(
+			:post,
+			"https://dashboard.bandwidth.com/v1.0/accounts//disconnects"
+		).with(
+			body: {
+				name: "test",
+				DisconnectTelephoneNumberOrderType: {
+					TelephoneNumberList: {
+						TelephoneNumber: "5556667777"
+					}
+				}
+			}.to_xml(indent: 0, root: "DisconnectTelephoneNumberOrder")
+		).to_return(status: 200, body: "")
+
+		admin.action_cancel_account.sync
+
+		assert_mock sgx
+		assert_mock BackendSgx::IQ_MANAGER
+		assert_mock Customer::BLATHER
+	end
+	em :test_action_cancel_account
+
+	def test_action_cancel_account_keep_number
+		sgx, admin = admin_command("+15566667777")
+
+		Customer::BLATHER.expect(
+			:<<,
+			EMPromise.resolve(nil),
+			[
+				Matching.new do |m|
+					assert_equal "Your JMP account has been cancelled.", m.body
+					assert_equal "test@example.net", m.to.to_s
+					assert_equal "notify_from@component", m.from.to_s
+				end
+			]
+		)
+
+		Customer::BLATHER.expect(
+			:<<,
+			EMPromise.resolve(nil),
+			[
+				Matching.new do |iq|
+					assert iq.remove?
+					assert_equal "test@example.net", iq.to.to_s
+					assert_equal "component", iq.from.to_s
+				end
+			]
+		)
+
+		sgx.expect(:deregister!, EMPromise.resolve(nil))
+
+		stub_request(
+			:post,
+			"https://dashboard.bandwidth.com/v1.0/accounts/moveto/moveTns"
+		).with(
+			body: {
+				SiteId: "movetosite",
+				CustomerOrderId: "test",
+				SourceAccountId: "test_bw_account",
+				TelephoneNumbers: { TelephoneNumber: "5566667777" }
+			}.to_xml(indent: 0, root: "MoveTnsOrder")
+		).to_return(status: 200, body: "")
+
+		admin.action_cancel_account.sync
+
+		assert_mock sgx
+		assert_mock BackendSgx::IQ_MANAGER
+		assert_mock Customer::BLATHER
+	end
+	em :test_action_cancel_account_keep_number
+end

test/test_helper.rb 🔗

@@ -98,6 +98,8 @@ CONFIG = {
 	},
 	credit_card_url: ->(*) { "http://creditcard.example.com" },
 	electrum_notify_url: ->(*) { "http://notify.example.com" },
+	keep_area_codes: ["556"],
+	keep_area_codes_in: { account: "moveto", site: "movetosite" },
 	upstream_domain: "example.net",
 	approved_domains: {
 		"approved.example.com": nil,