~singpolyma/sgx-jmp

e5759b337c8fc34256ea2df6ec4d9ea746f5574e — Stephen Paul Weber 8 months ago e6fa970 + 1c36e69
Merge branch 'tx-list'

* tx-list:
  Admin Command Menu + Admin Financial View
  Transactions List
  Customer Financials
  Telephone Link
A forms/admin_financial_info.rb => forms/admin_financial_info.rb +9 -0
@@ 0,0 1,9 @@
result!
title "Customer Financial Info"

field(
	var: "declines",
	label: "Declines",
	description: "out of 2",
	value: @info.declines.to_s
)

M forms/admin_info.rb => forms/admin_info.rb +8 -0
@@ 23,6 23,14 @@ field(
	value: @admin_info.customer_id
)

if @admin_info.info.tel
	field(
		var: "tel_link",
		label: "Phone Number Info",
		value: @admin_info.tel_link
	)
end

if @admin_info.fwd.uri
	field(
		var: "fwd",

A forms/admin_menu.rb => forms/admin_menu.rb +14 -0
@@ 0,0 1,14 @@
form!
title "Menu"

field(
	var: "action",
	type: "list-single",
	open: true,
	label: "Pick an action",
	description: "or put a new customer info",
	options: [
		{ value: "info", label: "Customer Info" },
		{ value: "financial", label: "Customer Billing Information" }
	]
)

A forms/admin_payment_methods.rb => forms/admin_payment_methods.rb +10 -0
@@ 0,0 1,10 @@
result!
title "Customer Payment Methods"

unless @payment_methods.empty?
	field @payment_methods.to_list_single(label: "Credit Cards")
end

AltTopUpForm::HasBitcoinAddresses.new(@btc_addresses, desc: nil).each do |spec|
	field spec
end

A forms/admin_transaction_list.rb => forms/admin_transaction_list.rb +10 -0
@@ 0,0 1,10 @@
result!
title "Transactions"

table(
	@transactions,
	formatted_amount: "Amount",
	note: "Note",
	created_at: "Date",
	transaction_id: "Transaction ID"
)

A forms/transactions.rb => forms/transactions.rb +9 -0
@@ 0,0 1,9 @@
result!
title "Transactions"

table(
	@transactions,
	formatted_amount: "Amount",
	note: "Note",
	created_at: "Date"
)

A lib/admin_command.rb => lib/admin_command.rb +79 -0
@@ 0,0 1,79 @@
# frozen_string_literal: true

require_relative "customer_info_form"
require_relative "financial_info"
require_relative "form_template"

class AdminCommand
	def initialize(target_customer)
		@target_customer = target_customer
	end

	def start
		action_info.then { menu }
	end

	def reply(form)
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << form
		}
	end

	def menu
		reply(FormTemplate.render("admin_menu")).then do |response|
			handle(response.form.field("action").value)
		end
	end

	def handle(action)
		if respond_to?("action_#{action}")
			send("action_#{action}")
		else
			new_context(action)
		end.then { menu }
	end

	def new_context(q)
		CustomerInfoForm.new.parse_something(q).then do |new_customer|
			if new_customer.respond_to?(:customer_id)
				AdminCommand.new(new_customer).start
			else
				reply(new_customer.form)
			end
		end
	end

	def action_info
		@target_customer.admin_info.then do |info|
			reply(info.form)
		end
	end

	def action_financial
		AdminFinancialInfo.for(@target_customer).then do |financial_info|
			reply(FormTemplate.render(
				"admin_financial_info",
				info: financial_info
			)).then {
				pay_methods(financial_info)
			}.then {
				transactions(financial_info)
			}
		end
	end

	def pay_methods(financial_info)
		reply(FormTemplate.render(
			"admin_payment_methods",
			**financial_info.to_h
		))
	end

	def transactions(financial_info)
		reply(FormTemplate.render(
			"admin_transaction_list",
			transactions: financial_info.transactions
		))
	end
end

M lib/alt_top_up_form.rb => lib/alt_top_up_form.rb +3 -2
@@ 101,8 101,9 @@ class AltTopUpForm
	end

	class HasBitcoinAddresses
		def initialize(addrs)
		def initialize(addrs, desc: DESCRIPTION)
			@addrs = addrs
			@desc = desc
		end

		DESCRIPTION =


@@ 116,7 117,7 @@ class AltTopUpForm
				var: "btc_address",
				type: "fixed",
				label: "Bitcoin Addresses",
				description: DESCRIPTION,
				description: @desc,
				value: @addrs
			)
		end

M lib/customer.rb => lib/customer.rb +8 -28
@@ 4,10 4,11 @@ require "forwardable"

require_relative "./api"
require_relative "./blather_ext"
require_relative "./customer_info"
require_relative "./customer_ogm"
require_relative "./customer_plan"
require_relative "./customer_usage"
require_relative "./customer_plan"
require_relative "./customer_ogm"
require_relative "./customer_info"
require_relative "./customer_finacials"
require_relative "./backend_sgx"
require_relative "./ibr"
require_relative "./payment_methods"


@@ 27,6 28,9 @@ class Customer
	def_delegators :@sgx, :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 initialize(
		customer_id,


@@ 38,6 42,7 @@ class Customer
	)
		@plan = plan
		@usage = CustomerUsage.new(customer_id)
		@financials = CustomerFinancials.new(customer_id)
		@customer_id = customer_id
		@jid = jid
		@balance = balance


@@ 61,14 66,6 @@ class Customer
		)
	end

	def payment_methods
		BRAINTREE
			.customer
			.find(@customer_id)
			.catch { OpenStruct.new(payment_methods: []) }
			.then(PaymentMethods.method(:for_braintree_customer))
	end

	def unused_invites
		promise = DB.query_defer(<<~SQL, [customer_id])
			SELECT code FROM unused_invites WHERE creator_id=$1


@@ 105,23 102,6 @@ class Customer
		sip_account.with_random_password.put
	end

	def btc_addresses
		REDIS.smembers("jmp_customer_btc_addresses-#{customer_id}")
	end

	def add_btc_address
		REDIS.spopsadd([
			"jmp_available_btc_addresses",
			"jmp_customer_btc_addresses-#{customer_id}"
		]).then do |addr|
			ELECTRUM.notify(
				addr,
				CONFIG[:electrum_notify_url].call(addr, customer_id)
			)
			addr
		end
	end

	def admin?
		CONFIG[:admins].include?(jid.to_s)
	end

A lib/customer_finacials.rb => lib/customer_finacials.rb +74 -0
@@ 0,0 1,74 @@
# frozen_string_literal: true

class CustomerFinancials
	def initialize(customer_id)
		@customer_id = customer_id
	end

	def payment_methods
		BRAINTREE
			.customer
			.find(@customer_id)
			.catch { OpenStruct.new(payment_methods: []) }
			.then(PaymentMethods.method(:for_braintree_customer))
	end

	def btc_addresses
		REDIS.smembers("jmp_customer_btc_addresses-#{@customer_id}")
	end

	def add_btc_address
		REDIS.spopsadd([
			"jmp_available_btc_addresses",
			"jmp_customer_btc_addresses-#{@customer_id}"
		]).then do |addr|
			ELECTRUM.notify(
				addr,
				CONFIG[:electrum_notify_url].call(addr, @customer_id)
			)
			addr
		end
	end

	def declines
		REDIS.get("jmp_pay_decline-#{@customer_id}")
	end

	def mark_decline
		REDIS.incr("jmp_pay_decline-#{@customer_id}").then do
			REDIS.expire("jmp_pay_decline-#{@customer_id}", 60 * 60 * 24)
		end
	end

	class TransactionInfo
		value_semantics do
			transaction_id String
			created_at Time
			amount BigDecimal
			note String
		end

		def formatted_amount
			"$%.4f" % amount
		end
	end

	TRANSACTIONS_SQL = <<~SQL
		SELECT
			transaction_id,
			created_at,
			amount,
			note
		FROM transactions WHERE customer_id = $1;
	SQL

	def transactions
		txns = DB.query_defer(TRANSACTIONS_SQL, [@customer_id])

		txns.then do |rows|
			rows.map { |row|
				TransactionInfo.new(**row.transform_keys(&:to_sym))
			}
		end
	end
end

M lib/customer_info.rb => lib/customer_info.rb +9 -0
@@ 118,4 118,13 @@ class AdminInfo
	def form
		FormTemplate.render("admin_info", admin_info: self)
	end

	def tel_link
		[
			"https://dashboard.bandwidth.com/portal/r/a",
			CONFIG[:creds][:account],
			"numbers/details",
			info.tel.gsub(/\A\+1/, "")
		].join("/")
	end
end

A lib/financial_info.rb => lib/financial_info.rb +25 -0
@@ 0,0 1,25 @@
# frozen_string_literal: true

require "value_semantics/monkey_patched"

class AdminFinancialInfo
	value_semantics do
		transactions ArrayOf(CustomerFinancials::TransactionInfo)
		declines Integer
		btc_addresses ArrayOf(String)
		payment_methods PaymentMethods
	end

	def self.for(customer)
		EMPromise.all([
			customer.transactions, customer.declines,
			customer.payment_methods, customer.btc_addresses
		]).then do |transactions, declines, payment_methods, btc_addresses|
			new(
				transactions: transactions,
				declines: declines || 0,
				payment_methods: payment_methods, btc_addresses: btc_addresses
			)
		end
	end
end

M lib/form_template.rb => lib/form_template.rb +11 -0
@@ 80,6 80,17 @@ class FormTemplate
			open || regex || range
		end

		# Given a map of fields to labels, and a list of objects this will
		# produce a table from calling each field's method on every object in the
		# list. So, this list is value_semantics / OpenStruct style
		def table(list, **fields)
			keys = fields.keys
			FormTable.new(
				list.map { |x| keys.map { |k| x.public_send(k) } },
				**fields
			).add_to_form(@__form)
		end

		def field(datatype: nil, open: false, regex: nil, range: nil, **kwargs)
			f = Blather::Stanza::X::Field.new(kwargs)
			if datatype || open || regex || range

M lib/payment_methods.rb => lib/payment_methods.rb +11 -1
@@ 50,7 50,13 @@ class PaymentMethods
		false
	end

	class Empty
	def to_a
		@methods
	end

	class Empty < PaymentMethods
		def initialize; end

		def default_payment_method; end

		def to_list_single(*)


@@ 60,5 66,9 @@ class PaymentMethods
		def empty?
			true
		end

		def to_a
			[]
		end
	end
end

M lib/transaction.rb => lib/transaction.rb +2 -4
@@ 4,7 4,7 @@ require "bigdecimal"

class Transaction
	def self.sale(customer, amount:, payment_method: nil)
		REDIS.get("jmp_pay_decline-#{customer.customer_id}").then do |declines|
		customer.declines.then do |declines|
			raise "too many declines" if declines.to_i >= 2

			BRAINTREE.transaction.sale(


@@ 20,9 20,7 @@ class Transaction
	def self.decline_guard(customer, response)
		return if response.success?

		REDIS.incr("jmp_pay_decline-#{customer.customer_id}").then do
			REDIS.expire("jmp_pay_decline-#{customer.customer_id}", 60 * 60 * 24)
		end
		customer.mark_decline
		raise response.message
	end


M sgx_jmp.rb => sgx_jmp.rb +14 -5
@@ 67,6 67,7 @@ end

require_relative "lib/polyfill"
require_relative "lib/alt_top_up_form"
require_relative "lib/admin_command"
require_relative "lib/add_bitcoin_address"
require_relative "lib/backend_sgx"
require_relative "lib/bwmsgsv2_repo"


@@ 473,6 474,18 @@ Command.new(
}.register(self).then(&CommandList.method(:register))

Command.new(
	"transactions",
	"Show Transactions",
	list_for: ->(customer:, **) { !!customer&.currency }
) {
	Command.customer.then(&:transactions).then do |txs|
		Command.finish do |reply|
			reply.command << FormTemplate.render("transactions", transactions: txs)
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"configure calls",
	"Configure Calls",
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)


@@ 714,11 727,7 @@ Command.new(
		}.then { |response|
			CustomerInfoForm.new.find_customer(response)
		}.then do |target_customer|
			target_customer.admin_info.then do |info|
				Command.finish do |reply|
					reply.command << info.form
				end
			end
			AdminCommand.new(target_customer).start
		end
	end
}.register(self).then(&CommandList.method(:register))

M test/test_alt_top_up_form.rb => test/test_alt_top_up_form.rb +3 -3
@@ 6,7 6,7 @@ require "customer"

class AltTopUpFormTest < Minitest::Test
	def test_for
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]


@@ 19,7 19,7 @@ class AltTopUpFormTest < Minitest::Test
	em :test_for

	def test_for_addresses
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve(["testaddr"]),
			["jmp_customer_btc_addresses-test"]


@@ 32,7 32,7 @@ class AltTopUpFormTest < Minitest::Test
	em :test_for_addresses

	def test_for_cad
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]

M test/test_buy_account_credit_form.rb => test/test_buy_account_credit_form.rb +1 -1
@@ 17,7 17,7 @@ class BuyAccountCreditFormTest < Minitest::Test

	def test_for
		braintree_customer = Minitest::Mock.new
		Customer::BRAINTREE.expect(:customer, braintree_customer)
		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
		braintree_customer.expect(
			:find,
			EMPromise.resolve(OpenStruct.new(payment_methods: [])),

M test/test_customer.rb => test/test_customer.rb +8 -6
@@ 5,13 5,15 @@ require "customer"

Customer::BLATHER = Minitest::Mock.new
Customer::BRAINTREE = Minitest::Mock.new
Customer::ELECTRUM = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
Customer::DB = Minitest::Mock.new
Customer::IQ_MANAGER = Minitest::Mock.new
CustomerPlan::DB = Minitest::Mock.new
CustomerUsage::REDIS = Minitest::Mock.new
CustomerUsage::DB = Minitest::Mock.new
CustomerFinancials::REDIS = Minitest::Mock.new
CustomerFinancials::ELECTRUM = Minitest::Mock.new
CustomerFinancials::BRAINTREE = Minitest::Mock.new

class CustomerTest < Minitest::Test
	def test_bill_plan_activate


@@ 191,7 193,7 @@ class CustomerTest < Minitest::Test
	em :test_sip_account_error

	def test_btc_addresses
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve(["testaddr"]),
			["jmp_customer_btc_addresses-test"]


@@ 202,19 204,19 @@ class CustomerTest < Minitest::Test
	em :test_btc_addresses

	def test_add_btc_address
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:spopsadd,
			EMPromise.resolve("testaddr"),
			[["jmp_available_btc_addresses", "jmp_customer_btc_addresses-test"]]
		)
		Customer::ELECTRUM.expect(
		CustomerFinancials::ELECTRUM.expect(
			:notify,
			EMPromise.resolve(nil),
			["testaddr", "http://notify.example.com"]
		)
		assert_equal "testaddr", customer.add_btc_address.sync
		assert_mock Customer::REDIS
		assert_mock Customer::ELECTRUM
		assert_mock CustomerFinancials::REDIS
		assert_mock CustomerFinancials::ELECTRUM
	end
	em :test_add_btc_address
end

M test/test_low_balance.rb => test/test_low_balance.rb +2 -2
@@ 5,7 5,7 @@ require "low_balance"

ExpiringLock::REDIS = Minitest::Mock.new
CustomerPlan::REDIS = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
CustomerFinancials::REDIS = Minitest::Mock.new

class LowBalanceTest < Minitest::Test
	def test_for_locked


@@ 24,7 24,7 @@ class LowBalanceTest < Minitest::Test
			EMPromise.resolve(0),
			["jmp_customer_low_balance-test"]
		)
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]

M test/test_registration.rb => test/test_registration.rb +4 -4
@@ 254,7 254,7 @@ class RegistrationTest < Minitest::Test
	end

	class PaymentTest < Minitest::Test
		Customer::BRAINTREE = Minitest::Mock.new
		CustomerFinancials::BRAINTREE = Minitest::Mock.new

		def test_for_bitcoin
			cust = Minitest::Mock.new(customer)


@@ 273,7 273,7 @@ class RegistrationTest < Minitest::Test

		def test_for_credit_card
			braintree_customer = Minitest::Mock.new
			Customer::BRAINTREE.expect(
			CustomerFinancials::BRAINTREE.expect(
				:customer,
				braintree_customer
			)


@@ 313,7 313,7 @@ class RegistrationTest < Minitest::Test

		class BitcoinTest < Minitest::Test
			Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new
			Customer::REDIS = Minitest::Mock.new
			CustomerFinancials::REDIS = Minitest::Mock.new

			def setup
				@customer = Minitest::Mock.new(


@@ 330,7 330,7 @@ class RegistrationTest < Minitest::Test
			end

			def test_write
				Customer::REDIS.expect(
				CustomerFinancials::REDIS.expect(
					:smembers,
					EMPromise.resolve([]),
					["jmp_customer_btc_addresses-test"]

M test/test_transaction.rb => test/test_transaction.rb +6 -4
@@ 18,17 18,17 @@ class TransactionTest < Minitest::Test
		)

	def test_sale_fails
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:incr,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:expire,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test", 60 * 60 * 24]


@@ 49,11 49,12 @@ class TransactionTest < Minitest::Test
				payment_method: OpenStruct.new(token: "token")
			).sync
		end
		assert_mock CustomerFinancials::REDIS
	end
	em :test_sale_fails

	def test_sale
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]


@@ 81,6 82,7 @@ class TransactionTest < Minitest::Test
			payment_method: OpenStruct.new(token: "token")
		).sync
		assert_kind_of Transaction, result
		assert_mock CustomerFinancials::REDIS
	end
	em :test_sale