New signup: go to web to choose credit card

Stephen Paul Weber created

This step, if chosen, directs the user to the jmp-pay webapp to add a credit
card.  It includes an OOB element for use by user agents that can handle
that (possibly for webview embed, etc) with a note-based fallback as per XEP.
Once the user chooses "next" the code checks if they have actually added a card,
and if so continues to a yet-unimplemented step and if not repeats the
instruction to go to the web app.

Change summary

config.dhall.sample                  |  2 +
lib/customer.rb                      |  1 
lib/payment_methods.rb               |  4 +
lib/registration.rb                  | 50 +++++++++++++++++++++++++++++
sgx_jmp.rb                           |  3 -
test/test_buy_account_credit_form.rb |  1 
test/test_helper.rb                  |  3 +
test/test_payment_methods.rb         |  2 
test/test_registration.rb            | 41 +++++++++++++++++++++++-
9 files changed, 98 insertions(+), 9 deletions(-)

Detailed changes

config.dhall.sample 🔗

@@ -24,4 +24,6 @@
 		}
 	},
 	plans = ./plans.dhall
+	credit_card_url = \(jid: Text) -> \(customer_id: Text) ->
+		"https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}"
 }

lib/customer.rb 🔗

@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require_relative "./ibr"
 require_relative "./payment_methods"
 require_relative "./plan"
 

lib/payment_methods.rb 🔗

@@ -19,7 +19,7 @@ class PaymentMethods
 	end
 
 	def default_payment_method
-		@methods.index(&:default?).to_s
+		@methods.index(&:default?)&.to_s
 	end
 
 	def to_options
@@ -43,6 +43,8 @@ class PaymentMethods
 	end
 
 	class Empty
+		def default_payment_method; end
+
 		def to_list_single(*)
 			raise "No payment methods available"
 		end

lib/registration.rb 🔗

@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative "./oob"
+
 class Registration
 	def self.for(iq, customer, web_register_manager)
 		raise "TODO" if customer&.active?
@@ -108,7 +110,7 @@ class Registration
 			when "bitcoin"
 				Bitcoin.new(iq, customer, tel)
 			when "credit_card"
-				raise "TODO"
+				CreditCard.for(iq, customer, tel)
 			when "code"
 				raise "TODO"
 			else
@@ -165,5 +167,51 @@ class Registration
 				end
 			end
 		end
+
+		class CreditCard
+			def self.for(iq, customer, tel)
+				customer.payment_methods.then do |payment_methods|
+					if payment_methods.default_payment_method
+						Activate.new(iq, customer, tel)
+					else
+						new(iq, customer, tel)
+					end
+				end
+			end
+
+			def initialize(iq, customer, tel)
+				@customer = customer
+				@tel = tel
+
+				@reply = iq.reply
+				@reply.allowed_actions = [:next]
+				@reply.note_type = :info
+				@reply.note_text = "#{oob.desc}: #{oob.url}"
+			end
+
+			attr_reader :reply
+
+			def oob
+				oob = OOB.find_or_create(@reply.command)
+				oob.url = CONFIG[:credit_card_url].call(
+					@reply.to.stripped.to_s,
+					@customer.customer_id
+				)
+				oob.desc = "Add credit card, then return here and choose next"
+				oob
+			end
+
+			def write
+				COMMAND_MANAGER.write(@reply).then do |riq|
+					CreditCard.for(riq, @customer, @tel)
+				end
+			end
+
+			class Activate
+				def initialize(_iq, _customer, _tel)
+					raise "TODO"
+				end
+			end
+		end
 	end
 end

sgx_jmp.rb 🔗

@@ -13,7 +13,6 @@ require_relative "lib/buy_account_credit_form"
 require_relative "lib/customer"
 require_relative "lib/electrum"
 require_relative "lib/em"
-require_relative "lib/existing_registration"
 require_relative "lib/payment_methods"
 require_relative "lib/registration"
 require_relative "lib/transaction"
@@ -21,7 +20,7 @@ require_relative "lib/web_register_manager"
 
 CONFIG =
 	Dhall::Coder
-	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
+	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
 	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
 
 ELECTRUM = Electrum.new(**CONFIG[:electrum])

test/test_buy_account_credit_form.rb 🔗

@@ -42,7 +42,6 @@ class BuyAccountCreditFormTest < Minitest::Test
 					type: "list-single",
 					var: "payment_method",
 					label: "Credit card to pay with",
-					value: "",
 					required: true,
 					options: [{ label: "Test 1234", value: "0" }]
 				),

test/test_helper.rb 🔗

@@ -52,7 +52,8 @@ CONFIG = {
 		merchant_accounts: {
 			USD: "merchant_usd"
 		}
-	}
+	},
+	credit_card_url: ->(*) { "http://creditcard.example.com" }
 }.freeze
 
 BLATHER = Class.new {

test/test_payment_methods.rb 🔗

@@ -52,7 +52,7 @@ class PaymentMethodsTest < Minitest::Test
 				type: "list-single",
 				label: "Credit card to pay with",
 				required: true,
-				value: "",
+				value: nil,
 				options: [
 					{ value: "0", label: "Test 1234" }
 				]

test/test_registration.rb 🔗

@@ -61,6 +61,7 @@ class RegistrationTest < Minitest::Test
 	end
 
 	class PaymentTest < Minitest::Test
+		Customer::BRAINTREE = Minitest::Mock.new
 		Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
 
 		def test_for_bitcoin
@@ -79,15 +80,30 @@ class RegistrationTest < Minitest::Test
 		end
 
 		def test_for_credit_card
-			skip "CreditCard not implemented yet"
+			braintree_customer = Minitest::Mock.new
+			Customer::BRAINTREE.expect(
+				:customer,
+				braintree_customer
+			)
+			braintree_customer.expect(
+				:find,
+				EMPromise.resolve(OpenStruct.new(payment_methods: [])),
+				["test"]
+			)
 			iq = Blather::Stanza::Iq::Command.new
+			iq.from = "test@example.com"
 			iq.form.fields = [
 				{ var: "activation_method", value: "credit_card" },
 				{ var: "plan_name", value: "test_usd" }
 			]
-			result = Registration::Payment.for(iq, "test", "+15555550000")
+			result = Registration::Payment.for(
+				iq,
+				Customer.new("test"),
+				"+15555550000"
+			).sync
 			assert_kind_of Registration::Payment::CreditCard, result
 		end
+		em :test_for_credit_card
 
 		def test_for_code
 			skip "Code not implemented yet"
@@ -150,5 +166,26 @@ class RegistrationTest < Minitest::Test
 			end
 			em :test_write
 		end
+
+		class CreditCardTest < Minitest::Test
+			def setup
+				iq = Blather::Stanza::Iq::Command.new
+				iq.from = "test@example.com"
+				@credit_card = Registration::Payment::CreditCard.new(
+					iq,
+					Customer.new("test"),
+					"+15555550000"
+				)
+			end
+
+			def test_reply
+				assert_equal [:execute, :next], @credit_card.reply.allowed_actions
+				assert_equal(
+					"Add credit card, then return here and choose next: " \
+					"http://creditcard.example.com",
+					@credit_card.reply.note.content
+				)
+			end
+		end
 	end
 end