~singpolyma/sgx-jmp

97df9261d037282f9473369e1384608172191ddd — Stephen Paul Weber 1 year, 24 days ago 9440485 + 051be0a
Merge branch 'cancel-account'

* cancel-account:
  Admin command to cancel customer
  Move Customer factory/extractor to Customer
M Gemfile => Gemfile +1 -1
@@ 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"

M config-schema.dhall => config-schema.dhall +2 -0
@@ 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

M config.dhall.sample => config.dhall.sample +2 -0
@@ 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" }
}

M forms/admin_menu.rb => forms/admin_menu.rb +2 -1
@@ 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" }
	]
)

M lib/admin_command.rb => lib/admin_command.rb +13 -0
@@ 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",

M lib/backend_sgx.rb => lib/backend_sgx.rb +7 -0
@@ 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)

M lib/bandwidth_tn_repo.rb => lib/bandwidth_tn_repo.rb +22 -0
@@ 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

M lib/customer.rb => lib/customer.rb +10 -1
@@ 26,13 26,21 @@ 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,
	               :add_btc_address, :declines, :mark_decline,
	               :transactions

	def self.extract(customer_id, jid, **kwargs)
		Customer.new(
			customer_id, jid,
			plan: CustomerPlan.extract(customer_id, kwargs),
			**kwargs.slice(:balance, :sgx, :tndetails)
		)
	end

	def initialize(
		customer_id,
		jid,


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

M lib/customer_repo.rb => lib/customer_repo.rb +15 -19
@@ 38,7 38,7 @@ class CustomerRepo
				redis.get("jmp_customer_jid-#{customer_id}").then do |jid|
					raise NotFound, "No jid" unless jid

					[customer_id, jid]
					[customer_id, Blather::JID.new(jid)]
				end
			end
		end


@@ 56,7 56,7 @@ class CustomerRepo
				redis.get("jmp_customer_id-#{jid}").then do |customer_id|
					raise NotFound, "No customer" unless customer_id

					[customer_id, jid]
					[customer_id, Blather::JID.new(jid)]
				end
			end
		end


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


@@ 173,29 178,20 @@ protected
		WHERE customer_id=$1 LIMIT 1
	SQL

	def fetch_all(customer_id)
		EMPromise.all([
			@sgx_repo.get(customer_id).then { |sgx| { sgx: sgx } },
			@db.query_one(SQL, customer_id, default: {}),
			fetch_redis(customer_id)
		]).then { |all| all.reduce(&:merge) }
	end

	def tndetails(sgx)
		return {} unless sgx.registered?

		LazyObject.new { @bandwidth_tn_repo.find(sgx.registered?.phone) || {} }
	end

	def find_inner(customer_id, jid)
		set_user.call(customer_id: customer_id, jid: jid)
		fetch_all(customer_id).then do |data|
			Customer.new(
				customer_id, Blather::JID.new(jid),
				tndetails: tndetails(data[:sgx]),
				plan: CustomerPlan.extract(customer_id, data),
				**data.slice(:balance, :sgx, :tndetails)
			)
	def find_inner(cid, jid)
		set_user.call(customer_id: cid, jid: jid)
		EMPromise.all([
			@sgx_repo.get(cid).then { |sgx| { sgx: sgx } },
			@db.query_one(SQL, cid, default: {}),
			fetch_redis(cid)
		]).then { |all| all.reduce(&:merge) }.then do |data|
			Customer.extract(cid, jid, tndetails: tndetails(data[:sgx]), **data)
		end
	end
end

M lib/ibr.rb => lib/ibr.rb +11 -0
@@ 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",

A test/test_admin_command.rb => test/test_admin_command.rb +115 -0
@@ 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

M test/test_helper.rb => test/test_helper.rb +2 -0
@@ 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,