~singpolyma/sgx-jmp

aa3117a59797562156d6a502c0fb4e310cef8367 — Stephen Paul Weber a month ago 6665370 + d969c9e
Merge branch 'whitelist'

* whitelist:
  Allow whitelisting domains
  Use FormTemplate for activation form
M .rubocop.yml => .rubocop.yml +1 -1
@@ 122,7 122,7 @@ Style/FormatStringToken:

Style/FrozenStringLiteralComment:
  Exclude:
    - forms/*
    - forms/**/*.rb

Naming/AccessorMethodName:
  Enabled: false

M Gemfile => Gemfile +1 -1
@@ 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"

M config-schema.dhall => config-schema.dhall +1 -0
@@ 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 :

M config.dhall.sample => config.dhall.sample +2 -1
@@ 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" }
}

A forms/registration/activate.rb => forms/registration/activate.rb +36 -0
@@ 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")

A forms/registration/allow.rb => forms/registration/allow.rb +10 -0
@@ 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")

A forms/registration/plan_name.rb => forms/registration/plan_name.rb +16 -0
@@ 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"
		}
	]
)

M lib/registration.rb => lib/registration.rb +56 -73
@@ 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

M test/test_helper.rb => test/test_helper.rb +6 -2
@@ 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)

M test/test_registration.rb => test/test_registration.rb +138 -2
@@ 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