Alt top up command

Stephen Paul Weber created

To show mailing address, eTransfer address, Bitcoin addresses, and allow
generating a new Bitcoin address.

Change summary

config.dhall.sample              |   5 +
lib/add_bitcoin_address.rb       |  50 ++++++++++++++
lib/alt_top_up_form.rb           | 115 ++++++++++++++++++++++++++++++++++
lib/command_list.rb              |   5 +
sgx_jmp.rb                       |  24 +++++++
test/test_add_bitcoin_address.rb |  44 +++++++++++++
test/test_alt_top_up_form.rb     |  91 ++++++++++++++++++++++++++
test/test_command_list.rb        |  17 ++--
test/test_helper.rb              |   5 +
9 files changed, 347 insertions(+), 9 deletions(-)

Detailed changes

config.dhall.sample 🔗

@@ -63,5 +63,8 @@
 	oxr_app_id = "",
 	activation_amount = 15,
 	credit_card_url = \(jid: Text) -> \(customer_id: Text) ->
-		"https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}"
+		"https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}",
+	adr = "",
+	interac = "",
+	payable = ""
 }

lib/add_bitcoin_address.rb 🔗

@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class AddBitcoinAddress
+	def self.for(iq, alt_form, customer)
+		if alt_form.parse(iq.form)[:add_btc_address]
+			new(iq, customer)
+		else
+			DoNot.new(iq)
+		end
+	end
+
+	def initialize(iq, customer)
+		@reply = iq.reply
+		@reply.status = :completed
+		@customer = customer
+	end
+
+	def write
+		@customer.add_btc_address.then do |addr|
+			form.fields = [{
+				var: "btc_address",
+				type: "fixed",
+				label: "Bitcoin Address",
+				value: addr
+			}]
+			BLATHER << @reply
+		end
+	end
+
+protected
+
+	def form
+		form = @reply.form
+		form.type = :result
+		form.title = "New Bitcoin Address"
+		form.instructions = "Your new address has been created"
+		form
+	end
+
+	class DoNot
+		def initialize(iq)
+			@reply = iq.reply
+			@reply.status = :completed
+		end
+
+		def write
+			BLATHER << @reply
+		end
+	end
+end

lib/alt_top_up_form.rb 🔗

@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+class AltTopUpForm
+	def self.for(customer)
+		customer.btc_addresses.then do |addrs|
+			AltTopUpForm.new(*[
+				(IS_CAD if customer.currency == :CAD),
+				(HasBitcoinAddresses.new(addrs) unless addrs.empty?),
+				AddBtcAddressField.for(addrs)
+			].compact)
+		end
+	end
+
+	def initialize(*fields)
+		@fields = fields
+	end
+
+	def form
+		form = Blather::Stanza::X.new(:result)
+		form.type = :form
+		form.title = "Buy Account Credit"
+		form.instructions =
+			"Besides credit cards, we support payment by Bitcoin, postal mail, " \
+			"or in Canada by Interac eTransfer."
+
+		form.fields = fields.to_a
+		form
+	end
+
+	def parse(form)
+		{
+			add_btc_address: ["1", "true"].include?(
+				form.field("add_btc_address")&.value.to_s
+			)
+		}
+	end
+
+	MAILING_ADDRESS = {
+		var: "adr",
+		type: "fixed",
+		label: "Mailing Address",
+		description:
+			"Make payable to #{CONFIG[:payable]} and include your " \
+			"Jabber ID in the mailing somewhere.",
+		value: CONFIG[:adr]
+	}.freeze
+
+	def fields
+		Enumerator.new do |y|
+			y << MAILING_ADDRESS
+			@fields.each do |fs|
+				fs.each { |f| y << f }
+			end
+		end
+	end
+
+	IS_CAD = [
+		var: "adr",
+		type: "fixed",
+		label: "Interac eTransfer Address",
+		description: "Please include your Jabber ID in the note",
+		value: CONFIG[:interac]
+	].freeze
+
+	class AddBtcAddressField
+		def self.for(addrs)
+			if addrs.empty?
+				AddNewBtcAddressField.new
+			else
+				new
+			end
+		end
+
+		def each
+			yield(
+				var: "add_btc_address",
+				label: label,
+				type: "boolean",
+				value: false
+			)
+		end
+
+		def label
+			"Or, create a new Bitcoin address?"
+		end
+
+		class AddNewBtcAddressField < AddBtcAddressField
+			def label
+				"You have no Bitcoin addresses, would you like to create one?"
+			end
+		end
+	end
+
+	class HasBitcoinAddresses
+		def initialize(addrs)
+			@addrs = addrs
+		end
+
+		DESCRIPTION =
+			"You can make a Bitcoin payment of any amount to any " \
+			"of these addresses and it will be credited to your " \
+			"account at the Canadian Bitcoins exchange rate within 5 " \
+			"minutes of your transaction reaching 3 confirmations."
+
+		def each
+			yield(
+				var: "btc_address",
+				type: "fixed",
+				label: "Bitcoin Addresses",
+				description: DESCRIPTION,
+				value: @addrs
+			)
+		end
+	end
+end

lib/command_list.rb 🔗

@@ -24,6 +24,7 @@ class CommandList
 			]).then do |(fwd, payment_methods)|
 				Registered.new(*[
 					(HAS_CREDIT_CARD unless payment_methods.empty?),
+					(HAS_CURRENCY if customer.currency),
 					(HAS_FORWARDING if fwd)
 				].compact)
 			end
@@ -52,6 +53,10 @@ class CommandList
 		end
 	end
 
+	HAS_CURRENCY = [
+		node: "alt top up",
+		name: "Buy Account Credit by Bitcoin, Mail, or Interac eTransfer"
+	].freeze
 
 	HAS_FORWARDING = [
 		node: "record-voicemail-greeting",

sgx_jmp.rb 🔗

@@ -24,6 +24,8 @@ singleton_class.class_eval do
 	Blather::DSL.append_features(self)
 end
 
+require_relative "lib/alt_top_up_form"
+require_relative "lib/add_bitcoin_address"
 require_relative "lib/backend_sgx"
 require_relative "lib/bandwidth_tn_order"
 require_relative "lib/btc_sell_prices"
@@ -370,6 +372,28 @@ command :execute?, node: "top up", sessionid: nil do |iq|
 	}.catch { |e| panic(e, sentry_hub) }
 end
 
+command :execute?, node: "alt top up", sessionid: nil do |iq|
+	sentry_hub = new_sentry_hub(iq, name: iq.node)
+	reply = iq.reply
+	reply.status = :executing
+	reply.allowed_actions = [:complete]
+
+	Customer.for_jid(iq.from.stripped).then { |customer|
+		sentry_hub.current_scope.set_user(
+			id: customer.customer_id,
+			jid: iq.from.stripped.to_s
+		)
+
+		EMPromise.all([AltTopUpForm.for(customer), customer])
+	}.then { |(alt_form, customer)|
+		reply.command << alt_form.form
+
+		COMMAND_MANAGER.write(reply).then do |iq2|
+			AddBitcoinAddress.for(iq2, alt_form, customer).write
+		end
+	}.catch { |e| panic(e, sentry_hub) }
+end
+
 command :execute?, node: "reset sip account", sessionid: nil do |iq|
 	sentry_hub = new_sentry_hub(iq, name: iq.node)
 	Customer.for_jid(iq.from.stripped).then { |customer|

test/test_add_bitcoin_address.rb 🔗

@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "alt_top_up_form"
+require "add_bitcoin_address"
+
+class AddBitcoinAddressTest < Minitest::Test
+	def test_for
+		iq = Blather::Stanza::Iq::Command.new
+		AddBitcoinAddress.for(iq, AltTopUpForm.new, Customer.new("test"))
+	end
+
+	def test_for_add_bitcoin
+		iq = Blather::Stanza::Iq::Command.new
+		iq.form.fields = [{ var: "add_btc_address", value: "true" }]
+		AddBitcoinAddress.for(iq, AltTopUpForm.new, Customer.new("test"))
+	end
+
+	def test_write
+		customer = Minitest::Mock.new
+		customer.expect(:add_btc_address, EMPromise.resolve("newaddress"))
+		iq = Blather::Stanza::Iq::Command.new
+		AddBitcoinAddress.new(iq, customer).write.sync
+		assert_mock customer
+	end
+	em :test_write
+
+	class DoNotTest < Minitest::Test
+		AddBitcoinAddress::DoNot::BLATHER = Minitest::Mock.new
+
+		def test_write
+			AddBitcoinAddress::DoNot::BLATHER.expect(
+				:<<,
+				EMPromise.resolve(nil)
+			) do |stanza|
+				assert_equal :completed, stanza.status
+			end
+			iq = Blather::Stanza::Iq::Command.new
+			AddBitcoinAddress::DoNot.new(iq).write.sync
+			assert_mock AddBitcoinAddress::DoNot::BLATHER
+		end
+		em :test_write
+	end
+end

test/test_alt_top_up_form.rb 🔗

@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "alt_top_up_form"
+require "customer"
+
+class AltTopUpFormTest < Minitest::Test
+	def test_for
+		Customer::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["jmp_customer_btc_addresses-test"]
+		)
+		assert_kind_of(
+			AltTopUpForm,
+			AltTopUpForm.for(Customer.new("test")).sync
+		)
+	end
+	em :test_for
+
+	def test_for_addresses
+		Customer::REDIS.expect(
+			:smembers,
+			EMPromise.resolve(["testaddr"]),
+			["jmp_customer_btc_addresses-test"]
+		)
+		assert_kind_of(
+			AltTopUpForm,
+			AltTopUpForm.for(Customer.new("test")).sync
+		)
+	end
+	em :test_for_addresses
+
+	def test_for_cad
+		Customer::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["jmp_customer_btc_addresses-test"]
+		)
+		assert_kind_of(
+			AltTopUpForm,
+			AltTopUpForm.for(Customer.new("test", plan_name: "test_cad")).sync
+		)
+	end
+	em :test_for_cad
+
+	def test_form_addrs
+		assert_kind_of(
+			Blather::Stanza::X,
+			AltTopUpForm.new(AltTopUpForm::AddBtcAddressField.new).form
+		)
+	end
+
+	def test_form_new_addrs
+		assert_kind_of(
+			Blather::Stanza::X,
+			AltTopUpForm.new(
+				AltTopUpForm::AddBtcAddressField::AddNewBtcAddressField.new
+			).form
+		)
+	end
+
+	def test_parse_true
+		iq_form = Blather::Stanza::X.new
+		iq_form.fields = [
+			{ var: "add_btc_address", value: "true" }
+		]
+		assert AltTopUpForm.new.parse(iq_form)[:add_btc_address]
+	end
+
+	def test_parse_1
+		iq_form = Blather::Stanza::X.new
+		iq_form.fields = [
+			{ var: "add_btc_address", value: "1" }
+		]
+		assert AltTopUpForm.new.parse(iq_form)[:add_btc_address]
+	end
+
+	def test_parse_false
+		iq_form = Blather::Stanza::X.new
+		iq_form.fields = [
+			{ var: "add_btc_address", value: "false" }
+		]
+		refute AltTopUpForm.new.parse(iq_form)[:add_btc_address]
+	end
+
+	def test_parse_not_presend
+		iq_form = Blather::Stanza::X.new
+		refute AltTopUpForm.new.parse(iq_form)[:add_btc_address]
+	end
+end

test/test_command_list.rb 🔗

@@ -92,24 +92,25 @@ class CommandListTest < Minitest::Test
 	end
 	em :test_for_registered_with_credit_card
 
-	def test_for_registered_with_forwarding_and_billing
+	def test_for_registered_with_currency
 		CommandList::REDIS.expect(
 			:get,
-			EMPromise.resolve("tel:1"),
+			EMPromise.resolve(nil),
 			["catapult_fwd-1"]
 		)
 		CommandList::Customer.expect(
 			:for_jid,
 			EMPromise.resolve(OpenStruct.new(
 				registered?: OpenStruct.new(phone: "1"),
-				plan_name: "test",
-				payment_methods: EMPromise.resolve([:boop])
+				currency: :USD
 			)),
 			["registered"]
 		)
-		result = CommandList.for("registered").sync
-		assert_kind_of CommandList::HasForwarding, result
-		assert_kind_of CommandList::HasBilling, result
+
+		assert_equal(
+			CommandList::HAS_CURRENCY,
+			CommandList::HAS_CURRENCY & CommandList.for("registered").sync.to_a
+		)
 	end
-	em :test_for_registered_with_forwarding_and_billing
+	em :test_for_registered_with_currency
 end

test/test_helper.rb 🔗

@@ -62,6 +62,11 @@ CONFIG = {
 		{
 			name: "test_bad_currency",
 			currency: :BAD
+		},
+		{
+			name: "test_cad",
+			currency: :CAD,
+			monthly_price: 1000
 		}
 	],
 	braintree: {