~singpolyma/sgx-jmp

1d81fc9096353f7fea61b9b122558dc866321347 — Stephen Paul Weber 11 months ago 2169e8e + 37329d3
Merge branch 'rubocop'

* rubocop:
  Additional fixes for rubocop 1.10.1
  Switch to rubocop 0.89.1
M .rubocop.yml => .rubocop.yml +52 -20
@@ 1,10 1,6 @@
AllCops:
  TargetRubyVersion: 2.5

Metrics/LineLength:
  Max: 80
  Exclude:
    - Gemfile
  NewCops: enable

Metrics/ClassLength:
  Exclude:


@@ 21,20 17,53 @@ Metrics/BlockLength:
  Exclude:
    - test/*

Metrics/ClassLength:
  Exclude:
    - test/*

Metrics/AbcSize:
  Exclude:
    - test/*

Style/Tab:
Metrics/ParameterLists:
  Max: 6

Naming/MethodParameterName:
  AllowNamesEndingInNumbers: false
  AllowedNames:
    - m
    - e
    - q
    - s
    - k
    - v
    - ex
    - tx
    - id
    - iq
    - db

Layout/IndentationStyle:
  Enabled: false
  EnforcedStyle: tabs
  IndentationWidth: 2

Style/IndentationWidth:
Layout/IndentationWidth:
  Width: 1 # one tab

Layout/LineLength:
  Max: 80
  Exclude:
    - Gemfile

Layout/SpaceAroundEqualsInParameterDefault:
  EnforcedStyle: no_space

Layout/AccessModifierIndentation:
  EnforcedStyle: outdent

Layout/FirstParameterIndentation:
  EnforcedStyle: consistent

Style/AccessModifierDeclarations:
  Enabled: false

Style/StringLiterals:
  EnforcedStyle: double_quotes



@@ 64,22 93,25 @@ Style/RegexpLiteral:
  EnforcedStyle: slashes
  AllowInnerSlashes: true

Layout/SpaceAroundEqualsInParameterDefault:
  EnforcedStyle: no_space

Layout/AccessModifierIndentation:
  EnforcedStyle: outdent
Lint/OutOfRangeRegexpRef:
  Enabled: false

Layout/FirstParameterIndentation:
  EnforcedStyle: consistent
Lint/MissingSuper:
  Enabled: false

Style/BlockDelimiters:
  EnforcedStyle: braces_for_chaining
  EnforcedStyle: semantic
  AllowBracesOnProceduralOneLiners: true
  ProceduralMethods:
    - execute_command

Style/MultilineBlockChain:
  Enabled: false

Layout/IndentArray:
Layout/FirstArgumentIndentation:
  EnforcedStyle: consistent

Layout/FirstArrayElementIndentation:
  EnforcedStyle: consistent

Style/FormatString:

M Gemfile => Gemfile +1 -1
@@ 10,8 10,8 @@ gem "dhall"
gem "em-hiredis"
gem "em-http-request", git: "https://github.com/singpolyma/em-http-request", branch: "fix-letsencrypt"
gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
gem "em-synchrony"
gem "em_promise.rb", "~> 0.0.3"
gem "em-synchrony"
gem "eventmachine"
gem "money-open-exchange-rates"
gem "multibases"

M lib/alt_top_up_form.rb => lib/alt_top_up_form.rb +2 -2
@@ 63,13 63,13 @@ class AltTopUpForm
		end
	end

	IS_CAD = [
	IS_CAD = [{
		var: "adr",
		type: "fixed",
		label: "Interac eTransfer Address",
		description: "Please include your Jabber ID in the note",
		value: CONFIG[:interac]
	].freeze
	}].freeze

	class AddBtcAddressField
		def self.for(addrs)

M lib/backend_sgx.rb => lib/backend_sgx.rb +1 -3
@@ 24,9 24,7 @@ class BackendSgx
		ibr.username = creds[:username]
		ibr.password = creds[:password]
		ibr.phone = tel
		IQ_MANAGER.write(ibr).then do
			with(registered?: irb)
		end
		IQ_MANAGER.write(ibr)
	end

	def stanza(s)

M lib/btc_sell_prices.rb => lib/btc_sell_prices.rb +3 -1
@@ 22,7 22,9 @@ class BTCSellPrices
			canadianbitcoins = Nokogiri::HTML.parse(http.response)

			bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
			raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin"
			unless bitcoin_row.at("td").text == "Bitcoin"
				raise "Bitcoin row has moved"
			end

			BigDecimal(
				bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1]

M lib/buy_account_credit_form.rb => lib/buy_account_credit_form.rb +2 -1
@@ 23,7 23,7 @@ class BuyAccountCreditForm
		@payment_methods = payment_methods
	end

	RANGE = (15..1000)
	RANGE = (15..1000).freeze

	AMOUNT_FIELD =
		XEP0122Field.new(


@@ 54,6 54,7 @@ class BuyAccountCreditForm
	def parse(form)
		amount = form.field("amount")&.value&.to_s
		raise AmountValidationError, amount unless RANGE.include?(amount.to_i)

		{
			payment_method: @payment_methods.fetch(
				form.field("payment_method")&.value.to_i

M lib/bwmsgsv2_repo.rb => lib/bwmsgsv2_repo.rb +1 -1
@@ 68,7 68,7 @@ protected
		ibr.from = from_jid

		IQ_MANAGER.write(ibr).catch { nil }.then do |result|
			if result&.respond_to?(:registered?) && result&.registered?
			if result.respond_to?(:registered?) && result.registered?
				result
			else
				false

M lib/catapult.rb => lib/catapult.rb +2 -0
@@ 27,6 27,7 @@ class Catapult
			unless http.response_header.status == 201
				raise "Create new SIP account failed"
			end

			http.response_header["location"]
		end
	end


@@ 81,6 82,7 @@ class Catapult
	def mkurl(path)
		base = "https://api.catapult.inetwork.com/v1/users/#{@user}/"
		return path if path.start_with?(base)

		"#{base}#{path}"
	end


M lib/command.rb => lib/command.rb +9 -8
@@ 58,9 58,9 @@ class Command
		end

		def reply(stanza=nil)
			stanza ||= iq.reply.tap do |reply|
			stanza ||= iq.reply.tap { |reply|
				reply.status = :executing
			end
			}
			yield stanza if block_given?
			COMMAND_MANAGER.write(stanza).then { |new_iq|
				@iq = new_iq


@@ 96,13 96,13 @@ class Command
		end

		def customer
			@customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then do |c|
			@customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then { |c|
				sentry_hub.current_scope.set_user(
					id: c.customer_id,
					jid: @iq.from.stripped
				)
				c
			end
			}
		end

	protected


@@ 112,7 112,7 @@ class Command
				next EMPromise.reject(iq) unless iq.cancel?

				finish(status: :canceled)
			}.catch_only(Timeout) {}.catch_only(FinalStanza) { |e|
			}.catch_only(Timeout) { nil }.catch_only(FinalStanza) { |e|
				@blather << e.stanza
			}.catch do |e|
				log_error(e)


@@ 143,17 143,18 @@ class Command
		name,
		customer_repo: CustomerRepo.new,
		list_for: ->(tel:, **) { !!tel },
		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s }
		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
		&blk
	)
		@node = node
		@name = name
		@customer_repo = customer_repo
		@list_for = list_for
		@format_error = format_error
		@blk = ->(exe) { yield exe }
		@blk = blk
	end

	def register(blather, guards: [:execute?, node: @node, sessionid: nil])
	def register(blather, guards: [:execute?, { node: @node, sessionid: nil }])
		blather.command(*guards) do |iq|
			Execution.new(@customer_repo, blather, @format_error, iq).execute(&@blk)
		end

M lib/configure_calls_form.rb => lib/configure_calls_form.rb +3 -3
@@ 21,14 21,14 @@ class ConfigureCallsForm
				result[:transcription_enabled] =
					["1", "true"].include?(params["voicemail_transcription"])
			end
			result[:lidb_name] = params["lidb_name"] if lidb_guard(params["lidb_name"])
			result[:lidb_name] = params["lidb_name"] if lidb_guard(params)
		end
	end

protected

	def lidb_guard(lidb_name)
		!lidb_name.to_s.strip.empty? &&
	def lidb_guard(params)
		!params["lidb_name"].to_s.strip.empty? &&
			!@customer.tndetails.dig(:features, :lidb)
	end


M lib/customer.rb => lib/customer.rb +5 -1
@@ 20,6 20,7 @@ class Customer
	extend Forwardable

	attr_reader :customer_id, :balance, :jid

	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,


@@ 109,7 110,10 @@ class Customer
			"jmp_available_btc_addresses",
			"jmp_customer_btc_addresses-#{customer_id}"
		]).then do |addr|
			ELECTRUM.notify(addr, CONFIG[:electrum_notify_url].call(addr, customer_id))
			ELECTRUM.notify(
				addr,
				CONFIG[:electrum_notify_url].call(addr, customer_id)
			)
			addr
		end
	end

M lib/customer_fwd.rb => lib/customer_fwd.rb +4 -5
@@ 7,6 7,7 @@ class CustomerFwd
	def self.for(uri:, timeout:)
		timeout = Timeout.new(timeout)
		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


@@ 35,9 36,7 @@ class CustomerFwd

	value_semantics do
		uri Either(String, NilClass)
		# rubocop:disable Style/RedundantSelf
		self.timeout Timeout, coerce: Timeout.method(:new)
		# rubocop:enable Style/RedundantSelf
		def_attr :timeout, Timeout, coerce: Timeout.method(:new)
	end

	def with(new_attrs)


@@ 45,11 44,11 @@ class CustomerFwd
	end

	def create_call(account)
		request = Bandwidth::ApiCreateCallRequest.new.tap do |cc|
		request = Bandwidth::ApiCreateCallRequest.new.tap { |cc|
			cc.to = to
			cc.call_timeout = timeout.to_i
			yield cc if block_given?
		end
		}
		BANDWIDTH_VOICE.create_call(account, body: request).data.call_id
	end


M lib/customer_info.rb => lib/customer_info.rb +4 -1
@@ 37,11 37,14 @@ class CustomerInfo
	end

	def next_renewal
		{ var: "Next renewal", value: expires_at.strftime("%Y-%m-%d") } if expires_at
		return unless expires_at

		{ var: "Next renewal", value: expires_at.strftime("%Y-%m-%d") }
	end

	def monthly_amount
		return unless plan.monthly_price

		{ var: "Renewal", value: "$%.4f / month" % plan.monthly_price }
	end


M lib/customer_ogm.rb => lib/customer_ogm.rb +4 -2
@@ 3,6 3,7 @@
module CustomerOGM
	def self.for(url, fetch_vcard_temp)
		return Media.new(url) if url

		TTS.for(fetch_vcard_temp)
	end



@@ 12,7 13,7 @@ module CustomerOGM
		end

		def to_render
			[:voicemail_ogm_media, locals: { url: @url }]
			[:voicemail_ogm_media, { locals: { url: @url } }]
		end
	end



@@ 30,6 31,7 @@ module CustomerOGM
		def [](k)
			value = @vcard[k]
			return if value.to_s.empty?

			value
		end



@@ 38,7 40,7 @@ module CustomerOGM
		end

		def to_render
			[:voicemail_ogm_tts, locals: { fn: fn }]
			[:voicemail_ogm_tts, { locals: { fn: fn } }]
		end
	end
end

M lib/customer_plan.rb => lib/customer_plan.rb +1 -0
@@ 8,6 8,7 @@ class CustomerPlan
	extend Forwardable

	attr_reader :expires_at

	def_delegator :@plan, :name, :plan_name
	def_delegators :@plan, :currency, :merchant_account, :monthly_price


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

			find_inner(customer_id, jid)
		end
	end


@@ 34,6 35,7 @@ class CustomerRepo
		else
			@redis.get("jmp_customer_id-#{jid}").then do |customer_id|
				next find_legacy_customer(jid) unless customer_id

				find_inner(customer_id, jid)
			end
		end


@@ 42,6 44,7 @@ class CustomerRepo
	def find_by_tel(tel)
		@redis.get("catapult_jid-#{tel}").then do |jid|
			raise NotFound, "No jid" unless jid

			find_by_jid(jid)
		end
	end


@@ 49,11 52,13 @@ class CustomerRepo
	def create(jid)
		@braintree.customer.create.then do |result|
			raise "Braintree customer create failed" unless result.success?

			cid = result.customer.id
			@redis.msetnx(
				"jmp_customer_id-#{jid}", cid, "jmp_customer_jid-#{cid}", jid
			).then do |redis_result|
				raise "Saving new customer to redis failed" unless redis_result == 1

				Customer.new(cid, Blather::JID.new(jid), sgx: new_sgx(cid))
			end
		end


@@ 65,8 70,7 @@ class CustomerRepo
			lidb_tn_groups: { lidb_tn_group: {
				telephone_numbers: [customer.registered?.phone.sub(/\A\+1/, "")],
				subscriber_information: lidb_name,
				use_type: "RESIDENTIAL",
				visibility: "PUBLIC"
				use_type: "RESIDENTIAL", visibility: "PUBLIC"
			} }
		)
	end


@@ 79,9 83,7 @@ class CustomerRepo

	def put_fwd(customer, customer_fwd)
		@sgx_repo.put_fwd(
			customer.customer_id,
			customer.registered?.phone,
			customer_fwd
			customer.customer_id, customer.registered?.phone, customer_fwd
		)
	end



@@ 94,6 96,7 @@ protected
	def find_legacy_customer(jid)
		@redis.lindex("catapult_cred-#{jid}", 3).then do |tel|
			raise NotFound, "No customer" unless tel

			LegacyCustomer.new(Blather::JID.new(jid), tel)
		end
	end


@@ 108,14 111,19 @@ protected
		end
	end

	SQL = <<~SQL
		SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
		FROM customer_plans LEFT JOIN balances USING (customer_id)
		WHERE customer_id=$1 LIMIT 1
	SQL

	def find_inner(customer_id, jid)
		result = @db.query_defer(<<~SQL, [customer_id])
			SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
			FROM customer_plans LEFT JOIN balances USING (customer_id)
			WHERE customer_id=$1 LIMIT 1
		SQL
		result = @db.query_defer(SQL, [customer_id])
		EMPromise.all([@sgx_repo.get(customer_id), result]).then do |(sgx, rows)|
			data = hydrate_plan(customer_id, rows.first&.transform_keys(&:to_sym) || {})
			data = hydrate_plan(
				customer_id,
				rows.first&.transform_keys(&:to_sym) || {}
			)
			Customer.new(customer_id, Blather::JID.new(jid), sgx: sgx, **data)
		end
	end

M lib/customer_usage.rb => lib/customer_usage.rb +1 -1
@@ 50,7 50,7 @@ class CustomerUsage
				"jmp_customer_outbound_messages-#{@customer_id}",
				day.strftime("%Y%m%d")
			).then { |c| [day, c.to_i] if c }
		}).then { |r| Hash[r.compact].tap { |h| h.default = 0 } }
		}).then { |r| r.compact.to_h.tap { |h| h.default = 0 } }
	end

	QUERY_FOR_MINUTES = <<~SQL

M lib/form_template.rb => lib/form_template.rb +9 -8
@@ 48,21 48,21 @@ class FormTemplate
			@__form.instructions = s
		end

		def validate(f, datatype: nil, **kwargs)
			Nokogiri::XML::Builder.with(f) do |x|
				x.validate(
		def validate(field, datatype: nil, **kwargs)
			Nokogiri::XML::Builder.with(field) do |xml|
				xml.validate(
					xmlns: "http://jabber.org/protocol/xdata-validate",
					datatype: datatype || "xs:string"
				) do
					x.basic unless validation_type(x, **kwargs)
					xml.basic unless validation_type(xml, **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
		def validation_type(xml, open: false, regex: nil, range: nil)
			xml.open if open
			xml.range(min: range.first, max: range.last) if range
			xml.regex(regex.source) if regex
			open || regex || range
		end



@@ 80,6 80,7 @@ class FormTemplate

		def form
			raise "Type never set" unless @__type_set

			@__form
		end
	end

M lib/low_balance.rb => lib/low_balance.rb +1 -0
@@ 41,6 41,7 @@ class LowBalance

	def btc_addresses_for_notification
		return if @btc_addresses.empty?

		"\nYou can buy credit by sending any amount of Bitcoin to one of " \
		"these addresses:\n#{@btc_addresses.join("\n")}"
	end

M lib/not_loaded.rb => lib/not_loaded.rb +1 -1
@@ 11,7 11,7 @@ class NotLoaded
		true
	end

	def method_missing(*) # rubocop:disable Style/MethodMissing
	def method_missing(*)
		raise NotLoadedError, "#{@name} not loaded"
	end
end

M lib/oob.rb => lib/oob.rb +4 -4
@@ 34,10 34,10 @@ class OOB < Blather::XMPPNode
		find("ns:url", ns: self.class.registered_ns).first&.content
	end

	def url=(u)
	def url=(url)
		remove_children :url
		i = Niceogiri::XML::Node.new(:url, document, namespace)
		i.content = u
		i.content = url
		self << i
	end



@@ 45,10 45,10 @@ class OOB < Blather::XMPPNode
		find("ns:desc", ns: self.class.registered_ns).first&.content
	end

	def desc=(d)
	def desc=(description)
		remove_children :desc
		i = Niceogiri::XML::Node.new(:desc, document, namespace)
		i.content = d
		i.content = description
		self << i
	end
end

M lib/paypal_done.rb => lib/paypal_done.rb +4 -3
@@ 2,9 2,10 @@

class PaypalDone
	MESSAGE =
		"\n\nPayPal users must now go to https://www.paypal.com/myaccount/autopay/ " \
		"and cancel their PayPal subscription to JMP. Then contact support and " \
		"provide them with your PayPal email address."
		"\n\nPayPal users must now go to " \
		"https://www.paypal.com/myaccount/autopay/ and cancel their PayPal " \
		"subscription to JMP. Then contact support and provide them with " \
		"your PayPal email address."

	def initialize(*); end


M lib/proxied_jid.rb => lib/proxied_jid.rb +2 -1
@@ 4,7 4,8 @@ require "delegate"
require "blather"

class ProxiedJID < SimpleDelegator
	ESCAPED = /20|22|26|27|2f|3a|3c|3e|40|5c/
	ESCAPED = /20|22|26|27|2f|3a|3c|3e|40|5c/.freeze

	def unproxied
		Blather::JID.new(
			node.gsub(/\\(#{ESCAPED})/) { |s|

M lib/registration.rb => lib/registration.rb +6 -5
@@ 110,9 110,9 @@ class Registration
				ACTIVATE_INSTRUCTION,
				CRYPTOCURRENCY_INSTRUCTION
			].each do |txt|
				form << Blather::XMPPNode.new(:instructions, form.document).tap do |i|
				form << Blather::XMPPNode.new(:instructions, form.document).tap { |i|
					i << txt
				end
				}
			end
		end



@@ 202,9 202,9 @@ class Registration
		protected

			def addr
				@addr ||= @customer.btc_addresses.then do |addrs|
				@addr ||= @customer.btc_addresses.then { |addrs|
					addrs.first || @customer.add_btc_address
				end
				}
			end
		end



@@ 380,6 380,7 @@ class Registration
							WHERE code=$2 AND used_by_id IS NULL
						SQL
						raise Invalid, "Not a valid invite code: #{code}" unless valid

						@customer.activate_plan_starting_now
					end
				end


@@ 459,7 460,7 @@ class Registration
				EMPromise.all([
					REDIS.del("pending_tel_for-#{@customer.jid}"),
					Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
						uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5 seconds / ring, 5 rings
						uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
					))
				])
			}.then do

M lib/statsd.rb => lib/statsd.rb +2 -2
@@ 3,7 3,7 @@
require "statsd-instrument"

# These are basically data, not code, I find them more readable on one line each
# rubocop:disable Metrics/LineLength
# rubocop:disable Layout/LineLength

Registration::Registered.extend StatsD::Instrument
Registration::Registered.statsd_count :write, "registration.registered"


@@ 30,4 30,4 @@ Registration::Payment::Mail.statsd_count :write, "registration.payment.mail"
Registration::Finish.extend StatsD::Instrument
Registration::Finish.statsd_count :write, "registration.finish"

# rubocop:enable Metrics/LineLength
# rubocop:enable Layout/LineLength

M lib/tel_selections.rb => lib/tel_selections.rb +2 -2
@@ 174,11 174,11 @@ class TelSelections
			}.each do |k, args|
				klass = const_set(
					args[0],
					Class.new(Q) do
					Class.new(Q) {
						define_method(:iris_query) do
							{ k => @q }
						end
					end
					}
				)

				args[1..-1].each do |regex|

M lib/transaction.rb => lib/transaction.rb +2 -0
@@ 59,6 59,7 @@ class Transaction

	def bonus
		return BigDecimal(0) if amount <= 15

		amount *
			case amount
			when (15..29.99)


@@ 89,6 90,7 @@ protected

	def insert_bonus
		return if bonus <= 0

		params = [@customer_id, "bonus_for_#{@transaction_id}", @created_at, bonus]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions

M lib/usage_report.rb => lib/usage_report.rb +2 -2
@@ 29,11 29,11 @@ class UsageReport
		total_minutes = 0

		FormTable.new(
			@report_for.first.downto(@report_for.last).map do |day|
			@report_for.first.downto(@report_for.last).map { |day|
				total_messages += @messages[day]
				total_minutes += @minutes[day]
				[day, @messages[day], @minutes[day]]
			end + [["Total", total_messages, total_minutes]],
			} + [["Total", total_messages, total_minutes]],
			day: "Day", messages: "Messages", minutes: "Minutes"
		)
	end

M sgx_jmp.rb => sgx_jmp.rb +13 -13
@@ 134,7 134,7 @@ class AsyncBraintree
	end

	def respond_to_missing?(m, *)
		@gateway.respond_to?(m)
		@gateway.respond_to?(m) || super
	end

	def method_missing(m, *args)


@@ 147,11 147,12 @@ class AsyncBraintree

	class PromiseChain < EMPromise
		def respond_to_missing?(*)
			false # We don't actually know what we respond to...
			false && super # We don't actually know what we respond to...
		end

		def method_missing(m, *args)
			return super if respond_to_missing?(m, *args)

			self.then { |o| o.public_send(m, *args) }
		end
	end


@@ 188,10 189,10 @@ when_ready do
	REDIS = EM::Hiredis.connect
	TEL_SELECTIONS = TelSelections.new
	BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
	DB = PG::EM::ConnectionPool.new(dbname: "jmp") do |conn|
	DB = PG::EM::ConnectionPool.new(dbname: "jmp") { |conn|
		conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)
		conn.type_map_for_queries = PG::BasicTypeMapForQueries.new(conn)
	end
	}

	DB.hold do |conn|
		conn.query("LISTEN low_balance")


@@ 224,9 225,9 @@ setup(
message to: /\Aaccount@/, body: /./ do |m|
	StatsD.increment("deprecated_account_bot")

	self << m.reply.tap do |out|
	self << m.reply.tap { |out|
		out.body = "This bot is deprecated. Please talk to xmpp:cheogram.com"
	end
	}
end

before(


@@ 304,7 305,8 @@ message do |m|
				BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
				BLATHER.say(
					CONFIG[:notify_admin],
					"#{customer.customer_id} has used #{usage} messages since #{today - 30}",
					"#{customer.customer_id} has used #{usage} " \
					"messages since #{today - 30}",
					:groupchat
				)
			end


@@ 376,13 378,13 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
	}.then { |customer|
		CommandList.for(customer)
	}.then { |list|
		reply.items = list.map do |item|
		reply.items = list.map { |item|
			Blather::Stanza::DiscoItems::Item.new(
				iq.to,
				item[:node],
				item[:name]
			)
		end
		}
		self << reply
	}.catch { |e| panic(e, sentry_hub) }
end


@@ 468,9 470,7 @@ Command.new(
			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
		}.then { Command.finish("Configuration saved!") }
	end
}.register(self).then(&CommandList.method(:register))



@@ 726,7 726,7 @@ command :execute?, node: "web-register" do |iq|
			IQ_MANAGER.write(Blather::Stanza::Iq::Command.new.tap { |cmd|
				cmd.to = CONFIG[:web_register][:to]
				cmd.node = "push-register"
				cmd.form.fields = [var: "to", value: jid]
				cmd.form.fields = [{ var: "to", value: jid }]
				cmd.form.type = "submit"
			}).then { |result|
				TEL_SELECTIONS.set(result.form.field("from")&.value.to_s.strip, tel)

M test/test_bandwidth_tn_order.rb => test/test_bandwidth_tn_order.rb +2 -2
@@ 64,7 64,7 @@ class BandwidthTNOrderTest < Minitest::Test
				:post,
				"https://api.catapult.inetwork.com/v1/users/catapult_user/phoneNumbers"
			).with(
				body: open(__dir__ + "/data/catapult_import_body.json").read.chomp,
				body: File.open("#{__dir__}/data/catapult_import_body.json").read.chomp,
				headers: {
					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
					"Content-Type" => "application/json"


@@ 90,7 90,7 @@ class BandwidthTNOrderTest < Minitest::Test
				:post,
				"https://api.catapult.inetwork.com/v1/users/catapult_user/phoneNumbers"
			).with(
				body: open(__dir__ + "/data/catapult_import_body.json").read.chomp,
				body: File.open("#{__dir__}/data/catapult_import_body.json").read.chomp,
				headers: {
					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
					"Content-Type" => "application/json"

M test/test_buy_account_credit_form.rb => test/test_buy_account_credit_form.rb +5 -3
@@ 18,9 18,11 @@ class BuyAccountCreditFormTest < Minitest::Test
	def test_for
		braintree_customer = Minitest::Mock.new
		Customer::BRAINTREE.expect(:customer, braintree_customer)
		braintree_customer.expect(:find, EMPromise.resolve(
			OpenStruct.new(payment_methods: [])
		), ["test"])
		braintree_customer.expect(
			:find,
			EMPromise.resolve(OpenStruct.new(payment_methods: [])),
			["test"]
		)

		assert_kind_of(
			BuyAccountCreditForm,

M test/test_customer_info_form.rb => test/test_customer_info_form.rb +3 -1
@@ 18,7 18,9 @@ class FakeRepo

	def find_by_jid(jid)
		EMPromise.resolve(nil).then do
			@customers.find { |cust| cust.jid.to_s == jid.to_s } || raise("No Customer")
			@customers.find { |cust|
				cust.jid.to_s == jid.to_s
			} || raise("No Customer")
		end
	end


M test/test_customer_ogm.rb => test/test_customer_ogm.rb +3 -3
@@ 23,7 23,7 @@ class CustomerOGMTest < Minitest::Test
		def test_to_render_empty_vcard
			vcard = Blather::Stanza::Iq::Vcard::Vcard.new
			assert_equal(
				[:voicemail_ogm_tts, locals: { fn: "a user of JMP.chat" }],
				[:voicemail_ogm_tts, { locals: { fn: "a user of JMP.chat" } }],
				CustomerOGM::TTS.new(vcard).to_render
			)
		end


@@ 32,7 32,7 @@ class CustomerOGMTest < Minitest::Test
			vcard = Blather::Stanza::Iq::Vcard::Vcard.new
			vcard["FN"] = "name"
			assert_equal(
				[:voicemail_ogm_tts, locals: { fn: "name" }],
				[:voicemail_ogm_tts, { locals: { fn: "name" } }],
				CustomerOGM::TTS.new(vcard).to_render
			)
		end


@@ 41,7 41,7 @@ class CustomerOGMTest < Minitest::Test
			vcard = Blather::Stanza::Iq::Vcard::Vcard.new
			vcard["NICKNAME"] = "name"
			assert_equal(
				[:voicemail_ogm_tts, locals: { fn: "name" }],
				[:voicemail_ogm_tts, { locals: { fn: "name" } }],
				CustomerOGM::TTS.new(vcard).to_render
			)
		end

M test/test_customer_repo.rb => test/test_customer_repo.rb +6 -3
@@ 146,9 146,12 @@ class CustomerRepoTest < Minitest::Test
		repo = mkrepo(redis: redis, braintree: braintree)
		braintree_customer = Minitest::Mock.new
		braintree.expect(:customer, braintree_customer)
		braintree_customer.expect(:create, EMPromise.resolve(
			OpenStruct.new(success?: true, customer: OpenStruct.new(id: "test"))
		))
		braintree_customer.expect(
			:create,
			EMPromise.resolve(
				OpenStruct.new(success?: true, customer: OpenStruct.new(id: "test"))
			)
		)
		redis.expect(
			:msetnx,
			EMPromise.resolve(1),

M test/test_low_balance.rb => test/test_low_balance.rb +1 -1
@@ 85,7 85,7 @@ class LowBalanceTest < Minitest::Test
			LowBalance::AutoTopUp::Transaction.expect(
				:sale,
				tx,
				[@customer, amount: 100]
				[@customer, { amount: 100 }]
			)
			@auto_top_up.notify!
			assert_mock tx

M test/test_oob.rb => test/test_oob.rb +10 -10
@@ 15,11 15,11 @@ class OOBTest < Minitest::Test
	end

	property(:new_with_attrs) { [string(:alnum), string] }
	def new_with_attrs(u, d)
		oob = OOB.new(u, desc: d)
	def new_with_attrs(url, description)
		oob = OOB.new(url, desc: description)
		assert_kind_of OOB, oob
		assert_equal u, oob.url
		assert_equal d, oob.desc
		assert_equal url, oob.url
		assert_equal description, oob.desc
	end

	def test_find_or_create_not_found


@@ 34,16 34,16 @@ class OOBTest < Minitest::Test
	end

	property(:url) { string(:alnum) }
	def url(u)
	def url(a_url)
		oob = OOB.new
		oob.url = u
		assert_equal u, oob.url
		oob.url = a_url
		assert_equal a_url, oob.url
	end

	property(:desc) { string }
	def desc(d)
	def desc(description)
		oob = OOB.new
		oob.desc = d
		assert_equal d, oob.desc
		oob.desc = description
		assert_equal description, oob.desc
	end
end

M test/test_registration.rb => test/test_registration.rb +3 -3
@@ 324,14 324,14 @@ class RegistrationTest < Minitest::Test
				)
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				msg = Registration::Payment::CreditCard::Activate::DECLINE_MESSAGE
				Command::COMMAND_MANAGER.expect(
					:write,
					EMPromise.reject(:test_result),
					[Matching.new do |reply|
						assert_equal :error, reply.note_type
						assert_equal(
							Registration::Payment::CreditCard::Activate::DECLINE_MESSAGE +
							": http://creditcard.example.com",
							"#{msg}: http://creditcard.example.com",
							reply.note.content
						)
					end]


@@ 557,7 557,7 @@ class RegistrationTest < Minitest::Test
				:post,
				"https://api.catapult.inetwork.com/v1/users/catapult_user/phoneNumbers"
			).with(
				body: open(__dir__ + "/data/catapult_import_body.json").read.chomp,
				body: File.open("#{__dir__}/data/catapult_import_body.json").read.chomp,
				headers: {
					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
					"Content-Type" => "application/json"

M test/test_xep0122_field.rb => test/test_xep0122_field.rb +4 -4
@@ 13,7 13,7 @@ class XEP0122FieldTest < Minitest::Test
			type: "text-single"
		).field

		example = Nokogiri::XML::Builder.new do |xml|
		example = Nokogiri::XML::Builder.new { |xml|
			xml.field(
				xmlns: "jabber:x:data",
				var: "v",


@@ 27,7 27,7 @@ class XEP0122FieldTest < Minitest::Test
					xml.range(min: 0, max: 3)
				end
			end
		end
		}

		assert_equal example.doc.root.to_xml, field.to_xml
	end


@@ 40,7 40,7 @@ class XEP0122FieldTest < Minitest::Test
			type: "text-single"
		).field

		example = Nokogiri::XML::Builder.new do |xml|
		example = Nokogiri::XML::Builder.new { |xml|
			xml.field(
				xmlns: "jabber:x:data",
				var: "v",


@@ 54,7 54,7 @@ class XEP0122FieldTest < Minitest::Test
					xml.basic
				end
			end
		end
		}

		assert_equal example.doc.root.to_xml, field.to_xml
	end

M web.rb => web.rb +8 -6
@@ 55,8 55,7 @@ class Web < Roda
	plugin RodaEMPromise # Must go last!

	class << self
		attr_reader :customer_repo, :log
		attr_reader :true_inbound_call, :outbound_transfers
		attr_reader :customer_repo, :log, :true_inbound_call, :outbound_transfers

		def run(log, *listen_on)
			plugin :common_logger, log, method: :info


@@ 118,7 117,7 @@ class Web < Roda
		elsif candidate[0] == "+" && /\A\d+\z/.match(candidate[1..-1])
			candidate
		elsif candidate == "Restricted"
			TEL_CANDIDATES.fetch(candidate, "19") +
			"#{TEL_CANDIDATES.fetch(candidate, '19')}" \
				";phone-context=anonymous.phone-context.soprani.ca"
		end
	end


@@ 157,6 156,7 @@ class Web < Roda
						call_id = params["callId"]
						EM.promise_timer(2).then {
							next unless true_inbound_call[p_call_id] == call_id

							true_inbound_call.delete(p_call_id)

							if (outbound_leg = outbound_transfers.delete(p_call_id))


@@ 257,13 257,13 @@ class Web < Roda
					CustomerRepo.new(
						sgx_repo: Bwmsgsv2Repo.new
					).find_by_tel(params["to"]).then(&:fwd).then do |fwd|
						call = fwd.create_call(CONFIG[:creds][:account]) do |cc|
						call = fwd.create_call(CONFIG[:creds][:account]) { |cc|
							true_inbound_call[pseudo_call_id] = params["callId"]
							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


@@ 288,7 288,9 @@ class Web < Roda

				r.post do
					customer_id = params["from"].sub(/^\+1/, "")
					CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new).find(customer_id).then do |c|
					CustomerRepo.new(
						sgx_repo: Bwmsgsv2Repo.new
					).find(customer_id).then do |c|
						render :forward, locals: {
							from: c.registered?.phone,
							to: params["to"]