Merge branch 'whitelist'

Stephen Paul Weber created

* whitelist:
  Allow whitelisting domains
  Use FormTemplate for activation form

Change summary

.rubocop.yml                    |   2 
Gemfile                         |   2 
config-schema.dhall             |   1 
config.dhall.sample             |   3 
forms/registration/activate.rb  |  36 +++++++++
forms/registration/allow.rb     |  10 ++
forms/registration/plan_name.rb |  16 ++++
lib/registration.rb             | 129 ++++++++++++++------------------
test/test_helper.rb             |   8 +
test/test_registration.rb       | 140 ++++++++++++++++++++++++++++++++++
10 files changed, 267 insertions(+), 80 deletions(-)

Detailed changes

.rubocop.yml 🔗

@@ -122,7 +122,7 @@ Style/FormatStringToken:
 
 Style/FrozenStringLiteralComment:
   Exclude:
-    - forms/*
+    - forms/**/*.rb
 
 Naming/AccessorMethodName:
   Enabled: false

Gemfile 🔗

@@ -6,7 +6,7 @@ gem "amazing_print"
 gem "bandwidth-sdk", "<= 6.1.0"
 gem "blather", git: "https://github.com/singpolyma/blather.git", branch: "ergonomics"
 gem "braintree"
-gem "dhall"
+gem "dhall", ">= 0.5.3.fixed"
 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"

config-schema.dhall 🔗

@@ -1,6 +1,7 @@
 { activation_amount : Natural
 , admins : List Text
 , adr : Text
+, approved_domains : List { mapKey : Text, mapValue : Optional Text }
 , bandwidth_peer : Text
 , bandwidth_site : Text
 , braintree :

config.dhall.sample 🔗

@@ -83,5 +83,6 @@ in
 	payable = "",
 	notify_from = "+15551234567@example.net",
 	admins = ["test\\40example.com@example.net"],
-	upstream_domain = "example.net"
+	upstream_domain = "example.net",
+	approved_domains = toMap { `example.com` = Some "customer_id" }
 }

forms/registration/activate.rb 🔗

@@ -0,0 +1,36 @@
+form!
+title "Activate JMP"
+
+center = " (#{@rate_center})" if @rate_center
+instructions <<~I
+	You've selected #{@tel}#{center} as your JMP number.
+	To activate your account, you can either deposit $#{CONFIG[:activation_amount]} to your balance or enter your invite code if you have one.
+	(If you'd like to pay in a cryptocurrency other than Bitcoin, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io. Manual payment via Bitcoin Cash is also available if you contact support.)
+I
+
+field(
+	var: "activation_method",
+	type: "list-single",
+	label: "Activate using",
+	required: true,
+	options: [
+		{
+			value: "credit_card",
+			label: "Credit Card"
+		},
+		{
+			value: "bitcoin",
+			label: "Bitcoin"
+		},
+		{
+			value: "code",
+			label: "Invite Code"
+		},
+		{
+			value: "mail",
+			label: "Mail or eTransfer"
+		}
+	]
+)
+
+instance_eval File.read("#{__dir__}/plan_name.rb")

forms/registration/allow.rb 🔗

@@ -0,0 +1,10 @@
+form!
+title "Activate JMP"
+
+center = " (#{@rate_center})" if @rate_center
+instructions <<~I
+	You've selected #{@tel}#{center} as your JMP number.
+	As a user of #{@domain} you will start out with a free trial for one month, after which you will need to top up your balance to keep the account.
+I
+
+instance_eval File.read("#{__dir__}/plan_name.rb")

forms/registration/plan_name.rb 🔗

@@ -0,0 +1,16 @@
+field(
+	var: "plan_name",
+	type: "list-single",
+	label: "What currency should your account balance be in?",
+	required: true,
+	options: [
+		{
+			value: "cad_beta_unlimited-v20210223",
+			label: "Canadian Dollars"
+		},
+		{
+			value: "usd_beta_unlimited-v20210223",
+			label: "United States Dollars"
+		}
+	]
+)

lib/registration.rb 🔗

@@ -36,8 +36,11 @@ class Registration
 		def self.for(customer, tel)
 			if customer.active?
 				Finish.new(customer, tel)
+			elsif CONFIG[:approved_domains].key?(customer.jid.domain.to_sym)
+				credit_to = CONFIG[:approved_domains][customer.jid.domain.to_sym]
+				Allow.new(customer, tel, credit_to)
 			else
-				EMPromise.resolve(new(customer, tel))
+				new(customer, tel)
 			end
 		end
 
@@ -48,85 +51,27 @@ class Registration
 
 		attr_reader :customer, :tel
 
-		FORM_FIELDS = [
-			{
-				var: "activation_method",
-				type: "list-single",
-				label: "Activate using",
-				required: true,
-				options: [
-					{
-						value: "credit_card",
-						label: "Credit Card"
-					},
-					{
-						value: "bitcoin",
-						label: "Bitcoin"
-					},
-					{
-						value: "code",
-						label: "Invite Code"
-					},
-					{
-						value: "mail",
-						label: "Mail or eTransfer"
-					}
-				]
-			},
-			{
-				var: "plan_name",
-				type: "list-single",
-				label: "What currency should your account balance be in?",
-				required: true,
-				options: [
-					{
-						value: "cad_beta_unlimited-v20210223",
-						label: "Canadian Dollars"
-					},
-					{
-						value: "usd_beta_unlimited-v20210223",
-						label: "United States Dollars"
-					}
-				]
-			}
-		].freeze
-
-		ACTIVATE_INSTRUCTION =
-			"To activate your account, you can either deposit " \
-			"$#{CONFIG[:activation_amount]} to your balance or enter " \
-			"your invite code if you have one."
-
-		CRYPTOCURRENCY_INSTRUCTION =
-			"(If you'd like to pay in a cryptocurrency other than " \
-			"Bitcoin, currently we recommend using a service like " \
-			"simpleswap.io, morphtoken.com, changenow.io, or godex.io. " \
-			"Manual payment via Bitcoin Cash is also available if you " \
-			"contact support.)"
-
-		def add_instructions(form, center)
-			center = " (#{center})" if center
-			[
-				"You've selected #{tel}#{center} as your JMP number",
-				ACTIVATE_INSTRUCTION,
-				CRYPTOCURRENCY_INSTRUCTION
-			].each do |txt|
-				form << Blather::XMPPNode.new(:instructions, form.document).tap { |i|
-					i << txt
-				}
-			end
+		def form(center)
+			FormTemplate.render(
+				"registration/activate",
+				tel: tel,
+				rate_center: center
+			)
 		end
 
 		def write
 			rate_center.then { |center|
 				Command.reply do |reply|
 					reply.allowed_actions = [:next]
-					form = reply.form
-					form.type = :form
-					form.title = "Activate JMP"
-					add_instructions(form, center)
-					form.fields = FORM_FIELDS
+					reply.command << form(center)
 				end
-			}.then { |iq| Payment.for(iq, customer, tel) }.then(&:write)
+			}.then(&method(:next_step))
+		end
+
+		def next_step(iq)
+			EMPromise.resolve(nil).then {
+				Payment.for(iq, customer, tel)
+			}.then(&:write)
 		end
 
 	protected
@@ -137,6 +82,44 @@ class Registration
 				"#{center[:rate_center]}, #{center[:state]}"
 			}.catch { nil }
 		end
+
+		class Allow < Activation
+			def initialize(customer, tel, credit_to)
+				super(customer, tel)
+				@credit_to = credit_to
+			end
+
+			def form(center)
+				FormTemplate.render(
+					"registration/allow",
+					tel: tel,
+					rate_center: center,
+					domain: customer.jid.domain
+				)
+			end
+
+			def next_step(iq)
+				plan_name = iq.form.field("plan_name").value.to_s
+				@customer = customer.with_plan(plan_name)
+				EMPromise.resolve(nil).then { activate }.then do
+					Finish.new(customer, tel).write
+				end
+			end
+
+		protected
+
+			def activate
+				DB.transaction do
+					if @credit_to
+						DB.exec(<<~SQL, [@credit_to, customer.customer_id])
+							INSERT INTO invites (creator_id, used_by_id, used_at)
+							VALUES ($1, $2, LOCALTIMESTAMP)
+						SQL
+					end
+					@customer.activate_plan_starting_now
+				end
+			end
+		end
 	end
 
 	module Payment

test/test_helper.rb 🔗

@@ -40,7 +40,7 @@ $VERBOSE = nil
 Sentry.init
 
 def customer(customer_id="test", plan_name: nil, **kwargs)
-	jid = Blather::JID.new("#{customer_id}@example.net")
+	jid = kwargs.delete(:jid) || Blather::JID.new("#{customer_id}@example.net")
 	if plan_name
 		expires_at = kwargs.delete(:expires_at) || Time.now
 		plan = CustomerPlan.new(
@@ -100,7 +100,11 @@ CONFIG = {
 	},
 	credit_card_url: ->(*) { "http://creditcard.example.com" },
 	electrum_notify_url: ->(*) { "http://notify.example.com" },
-	upstream_domain: "example.net"
+	upstream_domain: "example.net",
+	approved_domains: {
+		"approved.example.com": nil,
+		"refer.example.com": "refer_to"
+	}
 }.freeze
 
 def panic(e)

test/test_registration.rb 🔗

@@ -52,6 +52,22 @@ class RegistrationTest < Minitest::Test
 	end
 	em :test_for_activated
 
+	def test_for_not_activated_approved
+		sgx = OpenStruct.new(registered?: false)
+		web_manager = TelSelections.new(redis: FakeRedis.new)
+		web_manager.set("test@approved.example.com", "+15555550000")
+		iq = Blather::Stanza::Iq::Command.new
+		iq.from = "test@approved.example.com"
+		result = execute_command(iq) do
+			Registration.for(
+				customer(sgx: sgx, jid: Blather::JID.new("test@approved.example.com")),
+				web_manager
+			)
+		end
+		assert_kind_of Registration::Activation::Allow, result
+	end
+	em :test_for_not_activated_approved
+
 	def test_for_not_activated_with_customer_id
 		sgx = OpenStruct.new(registered?: false)
 		web_manager = TelSelections.new(redis: FakeRedis.new)
@@ -100,8 +116,8 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 (FA, KE) as your JMP number",
-						iq.form.instructions
+						"You've selected +15555550000 (FA, KE) as your JMP number.",
+						iq.form.instructions.lines.first.chomp
 					)
 				end]
 			)
@@ -114,6 +130,126 @@ class RegistrationTest < Minitest::Test
 		em :test_write
 	end
 
+	class AllowTest < Minitest::Test
+		Command::COMMAND_MANAGER = Minitest::Mock.new
+		Registration::Activation::Allow::DB = Minitest::Mock.new
+
+		def test_write_credit_to_nil
+			cust = Minitest::Mock.new(customer("test"))
+			allow = Registration::Activation::Allow.new(cust, "+15555550000", nil)
+
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/tns/+15555550000"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<TelephoneNumberResponse>
+					<TelephoneNumber>5555550000</TelephoneNumber>
+				</TelephoneNumberResponse>
+			RESPONSE
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/tns/5555550000/ratecenter"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<TelephoneNumberResponse>
+					<TelephoneNumberDetails>
+						<State>KE</State>
+						<RateCenter>FA</RateCenter>
+					</TelephoneNumberDetails>
+				</TelephoneNumberResponse>
+			RESPONSE
+			Command::COMMAND_MANAGER.expect(
+				:write,
+				EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+					iq.form.fields = [{ var: "plan_name", value: "test_usd" }]
+				}),
+				[Matching.new do |iq|
+					assert_equal :form, iq.form.type
+					assert_equal(
+						"You've selected +15555550000 (FA, KE) as your JMP number.",
+						iq.form.instructions.lines.first.chomp
+					)
+					assert_equal 1, iq.form.fields.length
+				end]
+			)
+			Registration::Activation::Allow::DB.expect(
+				:transaction,
+				EMPromise.reject(:test_result)
+			) do |&blk|
+				blk.call
+				true
+			end
+			cust.expect(:with_plan, cust, ["test_usd"])
+			cust.expect(:activate_plan_starting_now, nil)
+			assert_equal(
+				:test_result,
+				execute_command { allow.write.catch { |e| e } }
+			)
+			assert_mock Command::COMMAND_MANAGER
+		end
+		em :test_write_credit_to_nil
+
+		def test_write_credit_to_refercust
+			cust = Minitest::Mock.new(customer("test"))
+			allow = Registration::Activation::Allow.new(
+				cust, "+15555550000", "refercust"
+			)
+
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/tns/+15555550000"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<TelephoneNumberResponse>
+					<TelephoneNumber>5555550000</TelephoneNumber>
+				</TelephoneNumberResponse>
+			RESPONSE
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/tns/5555550000/ratecenter"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<TelephoneNumberResponse>
+					<TelephoneNumberDetails>
+						<State>KE</State>
+						<RateCenter>FA</RateCenter>
+					</TelephoneNumberDetails>
+				</TelephoneNumberResponse>
+			RESPONSE
+			Command::COMMAND_MANAGER.expect(
+				:write,
+				EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+					iq.form.fields = [{ var: "plan_name", value: "test_usd" }]
+				}),
+				[Matching.new do |iq|
+					assert_equal :form, iq.form.type
+					assert_equal(
+						"You've selected +15555550000 (FA, KE) as your JMP number.",
+						iq.form.instructions.lines.first.chomp
+					)
+					assert_equal 1, iq.form.fields.length
+				end]
+			)
+			Registration::Activation::Allow::DB.expect(
+				:transaction,
+				EMPromise.reject(:test_result)
+			) do |&blk|
+				blk.call
+				true
+			end
+			Registration::Activation::Allow::DB.expect(
+				:exec,
+				nil,
+				[String, ["refercust", "test"]]
+			)
+			cust.expect(:with_plan, cust, ["test_usd"])
+			cust.expect(:activate_plan_starting_now, nil)
+			assert_equal(
+				:test_result,
+				execute_command { allow.write.catch { |e| e } }
+			)
+			assert_mock Command::COMMAND_MANAGER
+		end
+		em :test_write_credit_to_refercust
+	end
+
 	class PaymentTest < Minitest::Test
 		Customer::BRAINTREE = Minitest::Mock.new