add registration w/ bch

Phillip Davis created

Change summary

forms/registration/bch.rb    | 19 ++++++++++
lib/btc_sell_prices.rb       | 48 ++++++++++++++++++++-----
lib/registration.rb          | 71 ++++++++++++++++++++++++++++++++-----
sgx_jmp.rb                   |  1 
test/test_bch_sell_prices.rb | 41 +++++++++++++++++++++
test/test_registration.rb    | 61 ++++++++++++++++++++++++++++++++
6 files changed, 219 insertions(+), 22 deletions(-)

Detailed changes

forms/registration/bch.rb 🔗

@@ -0,0 +1,19 @@
+result!
+title "Activate using Bitcoin Cash"
+
+field(
+	label: "Minimual initial Bitcoin Cash deposit for activation",
+	var: "amount",
+	value: "%.6f" % @amount
+)
+
+field(
+	label: "Bitcoin Cash address",
+	var: "bch_addresses",
+	value: @addr
+)
+
+instructions(
+	"You will received a notification when your payment is complete." \
+	"#{@final_message}"
+)

lib/btc_sell_prices.rb 🔗

@@ -8,7 +8,7 @@ require "nokogiri"
 
 require_relative "em"
 
-class BTCSellPrices
+class CryptoSellPrices
 	def initialize(redis, oxr_app_id)
 		@redis = redis
 		@oxr = Money::Bank::OpenExchangeRatesBank.new(
@@ -17,25 +17,33 @@ class BTCSellPrices
 		@oxr.app_id = oxr_app_id
 	end
 
+	def usd
+		EMPromise.all([cad, cad_to_usd]).then { |(a, b)| a * b }
+	end
+
+	def ticker_row_selector
+		raise NotImplementedError, "Subclass must implement"
+	end
+
+	def crypto_name
+		raise NotImplementedError, "Subclass must implement"
+	end
+
 	def cad
 		fetch_canadianbitcoins.then do |http|
-			canadianbitcoins = Nokogiri::HTML.parse(http.response)
+			cb = Nokogiri::HTML.parse(http.response)
 
-			bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
-			unless bitcoin_row.at("td").text == "Bitcoin"
-				raise "Bitcoin row has moved"
+			row = cb.at(self.ticker_row_selector)
+			unless row.at("td").text == self.crypto_name
+				raise "#{crypto_name} row has moved"
 			end
 
 			BigDecimal(
-				bitcoin_row.at("td:nth-of-type(4)").text.match(/^\$(\d+\.\d+)/)[1]
+				row.at("td:nth-of-type(4)").text.match(/^\$(\d+\.\d+)/)[1]
 			)
 		end
 	end
 
-	def usd
-		EMPromise.all([cad, cad_to_usd]).then { |(a, b)| a * b }
-	end
-
 protected
 
 	def fetch_canadianbitcoins
@@ -59,3 +67,23 @@ protected
 		end
 	end
 end
+
+class BCHSellPrices < CryptoSellPrices
+	def crypto_name
+		"Bitcoin Cash"
+	end
+
+	def ticker_row_selector
+		"#ticker > table > tbody > tr:nth-of-type(2)"
+	end
+end
+
+class BTCSellPrices < CryptoSellPrices
+	def crypto_name
+		"Bitcoin"
+	end
+
+	def ticker_row_selector
+		"#ticker > table > tbody > tr"
+	end
+end

lib/registration.rb 🔗

@@ -276,10 +276,18 @@ class Registration
 			}.call(customer, tel, final_message: final_message, finish: finish)
 		end
 
-		class Bitcoin
-			Payment.kinds[:bitcoin] = method(:new)
+		class CryptoPaymentMethod
+			def crypto_addrs
+				raise NotImplementedError, "Subclass must implement"
+			end
 
-			THIRTY_DAYS = 60 * 60 * 24 * 30
+			def reg_form_name
+				raise NotImplementedError, "Subclass must implement"
+			end
+
+			def sell_prices
+				raise NotImplementedError, "Subclass must implement"
+			end
 
 			def initialize(customer, tel, final_message: nil, **)
 				@customer = customer
@@ -288,17 +296,17 @@ class Registration
 				@final_message = final_message
 			end
 
-			attr_reader :customer_id, :tel
-
 			def save
 				TEL_SELECTIONS.set(@customer.jid, @tel)
 			end
 
+			attr_reader :customer_id, :tel
+
 			def form(rate, addr)
 				amount = CONFIG[:activation_amount] / rate
 
 				FormTemplate.render(
-					"registration/btc",
+					reg_form_name,
 					amount: amount,
 					addr: addr,
 					final_message: @final_message
@@ -314,9 +322,7 @@ class Registration
 					}.then(&method(:handle_possible_prev))
 				end
 			end
-
 		protected
-
 			def handle_possible_prev(iq)
 				raise "Action not allowed" unless iq.prev?
 
@@ -325,14 +331,57 @@ class Registration
 
 			def addr_and_rate
 				EMPromise.all([
-					@customer.btc_addresses.then { |addrs|
-						addrs.first || @customer.add_btc_address
+					self.crypto_addrs.then { |addrs|
+						addrs.first || self.add_crypto_addr
 					},
-					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
+
+					sell_prices.public_send(@customer.currency.to_s.downcase)
 				])
 			end
 		end
 
+		class Bitcoin < CryptoPaymentMethod
+			Payment.kinds[:bitcoin] = method(:new)
+
+			def reg_form_name
+				"registration/btc"
+			end
+
+			def sell_prices
+				BTC_SELL_PRICES
+			end
+
+			def crypto_addrs
+				@customer.btc_addresses
+			end
+
+			def add_crypto_addr
+				@customer.add_btc_address
+			end
+		end
+
+		## Like Bitcoin
+		class BCH < CryptoPaymentMethod
+			Payment.kinds[:bch] = method(:new)
+
+			def reg_form_name
+				"registration/bch"
+			end
+
+			def sell_prices
+				BCH_SELL_PRICES
+			end
+
+			def crypto_addrs
+				@customer.bch_addresses
+			end
+
+			def add_crypto_addr
+				@customer.add_bch_address
+			end
+		end
+
+
 		class CreditCard
 			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
 

sgx_jmp.rb 🔗

@@ -220,6 +220,7 @@ when_ready do
 	REDIS = EM::Hiredis.connect
 	MEMCACHE = EM::P::Memcache.connect
 	BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
+	BCH_SELL_PRICES = BCHSellPrices.new(REDIS, CONFIG[:oxr_app_id])
 	DB = Postgres.connect(dbname: "jmp", size: 5)
 	TEL_SELECTIONS = TelSelections.new
 

test/test_bch_sell_prices.rb 🔗

@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "em-hiredis"
+require "test_helper"
+require "btc_sell_prices"
+
+class BCHSellPricesTest < Minitest::Test
+	def setup
+		@redis = Minitest::Mock.new
+		@subject = BCHSellPrices.new(@redis, "")
+	end
+
+	def test_cad
+		stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+			body: "<div id='ticker'><table><tbody>" \
+			      "<tr>" \
+				  "<td>Monopoly Money</td><td></td><td></td><td>10 trillion</td>" \
+				  "</tr>" \
+				  "<tr>" \
+				  "<td>Bitcoin Cash</td><td></td><td></td><td>$123.00</td>" \
+				  "</tr>"
+		)
+		assert_equal BigDecimal(123), @subject.cad.sync
+	end
+	em :test_cad
+
+	def test_usd
+		stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+			body: "<div id='ticker'><table><tbody>" \
+			      "<tr>" \
+				  "<td>Monopoly Money</td><td></td><td></td><td>10 trillion</td>" \
+				  "</tr>" \
+				  "<tr>" \
+				  "<td>Bitcoin Cash</td><td></td><td></td><td>$123.00</td>" \
+				  "</tr>"
+		)
+		@redis.expect(:get, EMPromise.resolve("0.5"), ["cad_to_usd"])
+		assert_equal BigDecimal(123) / 2, @subject.usd.sync
+	end
+	em :test_usd
+end

test/test_registration.rb 🔗

@@ -574,6 +574,16 @@ class RegistrationTest < Minitest::Test
 			assert_kind_of Registration::Payment::Bitcoin, result
 		end
 
+		def test_for_bch
+			iq = Blather::Stanza::Iq::Command.new
+			iq.form.fields = [
+				{ var: "activation_method", value: "bch" },
+				{ var: "plan_name", value: "test_usd" }
+			]
+			result = Registration::Payment.for(iq, customer, "+15555550000")
+			assert_kind_of Registration::Payment::BCH, result
+		end
+
 		def test_for_credit_card
 			braintree_customer = Minitest::Mock.new
 			CustomerFinancials::BRAINTREE.expect(
@@ -655,7 +665,7 @@ class RegistrationTest < Minitest::Test
 					[Matching.new do |reply|
 						assert_equal :canceled, reply.status
 						assert_equal "1.000000", reply.form.field("amount").value
-						assert_equal "testaddr", reply.form.field("btc_addresses").value
+						assert_equal "testaddr", reply.form.field("bch_addresses").value
 						true
 					end]
 				)
@@ -673,6 +683,55 @@ class RegistrationTest < Minitest::Test
 			em :test_write
 		end
 
+		class BCHTest < Minitest::Test
+			Registration::Payment::BCH::BCH_SELL_PRICES = Minitest::Mock.new
+			CustomerFinancials::REDIS = Minitest::Mock.new
+
+			def setup
+				@customer = Minitest::Mock.new(
+					customer(plan_name: "test_usd")
+				)
+				@customer.expect(
+					:add_btc_address,
+					EMPromise.resolve("testaddr")
+				)
+				@bch = Registration::Payment::BCH.new(
+					@customer,
+					"+15555550000"
+				)
+			end
+
+			def test_write
+				CustomerFinancials::REDIS.expect(
+					:smembers,
+					EMPromise.resolve([]),
+					["jmp_customer_bch_addresses-test"]
+				)
+				blather = Minitest::Mock.new
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.reject(SessionManager::Timeout.new),
+					[Matching.new do |reply|
+						assert_equal :canceled, reply.status
+						assert_equal "1.000000", reply.form.field("amount").value
+						assert_equal "testaddr", reply.form.field("bch_addresses").value
+						true
+					end]
+				)
+				Registration::Payment::BCH::BCH_SELL_PRICES.expect(
+					:usd,
+					EMPromise.resolve(BigDecimal(1))
+				)
+				@bch.stub(:save, EMPromise.resolve(nil)) do
+					execute_command(blather: blather) do
+						@bch.write
+					end
+				end
+				assert_mock blather
+			end
+			em :test_write
+		end
+
 		class CreditCardTest < Minitest::Test
 			def setup
 				@credit_card = Registration::Payment::CreditCard.new(