charge for premium nums

Phillip Davis created

Change summary

forms/registration/activate.rb |   2 
lib/registration.rb            | 100 ++++++++++++++++++-------
test/test_registration.rb      | 138 ++++++++++++++++++++++++++++++++++++
3 files changed, 210 insertions(+), 30 deletions(-)

Detailed changes

forms/registration/activate.rb 🔗

@@ -3,7 +3,7 @@ title "Activate JMP"
 
 instructions <<~I
 	You've selected #{@tel} as your JMP number.
-	To activate your account, you can either deposit $#{CONFIG[:activation_amount]} to your balance or enter your referral code if you have one.
+	To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one.
 	(If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
 I
 

lib/registration.rb 🔗

@@ -16,6 +16,12 @@ require_relative "./proxied_jid"
 require_relative "./tel_selections"
 require_relative "./welcome_message"
 
+def reload_customer(customer)
+	EMPromise.resolve(nil).then do
+		Command.execution.customer_repo.find(customer.customer_id)
+	end
+end
+
 class Registration
 	def self.for(customer, google_play_userid, tel_selections)
 		if (reg = customer.registered?)
@@ -272,10 +278,11 @@ class Registration
 				raise NotImplementedError, "Subclass must implement"
 			end
 
-			def initialize(customer, tel, **)
+			def initialize(customer, tel, maybe_bill: MaybeBill, **)
 				@customer = customer
 				@customer_id = customer.customer_id
 				@tel = tel
+				@maybe_bill = maybe_bill
 			end
 
 			def save
@@ -285,11 +292,9 @@ class Registration
 			attr_reader :customer_id, :tel
 
 			def form(rate, addr)
-				amount = CONFIG[:activation_amount] / rate
-
 				FormTemplate.render(
 					reg_form_name,
-					amount: amount,
+					amount: @maybe_bill.price(@tel) / rate,
 					addr: addr
 				)
 			end
@@ -372,7 +377,7 @@ class Registration
 			end
 
 			def call
-				reload_customer.then do |customer|
+				reload_customer(@customer).then do |customer|
 					if customer.balance >= CONFIG[:activation_amount_accept]
 						next BillPlan.new(customer, @tel, finish: @finish)
 					end
@@ -381,19 +386,19 @@ class Registration
 				end
 			end
 
-			def reload_customer
-				EMPromise.resolve(nil).then do
-					Command.execution.customer_repo.find(@customer.customer_id)
-				end
-			end
-
 			def self.bill?
 				true
 			end
+
+			# @return [Float] The price of the number + activation fee
+			# @param [TelSelection::ChooseTel::Tn] tel The phone number to charge
+			def self.price(tel)
+				CONFIG[:activation_amount] + tel.price
+			end
 		end
 
 		class JustCharge
-			def initialize(customer)
+			def initialize(customer, *, **)
 				@customer = customer
 			end
 
@@ -406,25 +411,24 @@ class Registration
 			end
 		end
 
+		def self.price(tel)
+			tel.price
+		end
+
 		class CreditCard
 			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
 
 			def self.for(in_customer, tel, finish: Finish, maybe_bill: MaybeBill, **)
 				maybe_bill.new(in_customer, tel, finish: finish).call do |customer|
-					new(customer, tel, finish: finish)
+					new(customer, tel, finish: finish, maybe_bill: maybe_bill)
 				end
 			end
 
-			def self.reload_customer(customer)
-				EMPromise.resolve(nil).then do
-					Command.execution.customer_repo.find(customer.customer_id)
-				end
-			end
-
-			def initialize(customer, tel, finish: Finish)
+			def initialize(customer, tel, finish: Finish, maybe_bill: MaybeBill)
 				@customer = customer
 				@tel = tel
 				@finish = finish
+				@maybe_bill = maybe_bill
 			end
 
 			def oob(reply)
@@ -432,7 +436,7 @@ class Registration
 				oob.url = CONFIG[:credit_card_url].call(
 					reply.to.stripped.to_s.gsub("\\", "%5C"),
 					@customer.customer_id
-				) + "&amount=#{CONFIG[:activation_amount]}"
+				) + "&amount=#{@maybe_bill.price(@tel).ceil}"
 				oob.desc = "Pay by credit card, save, then next here to continue"
 				oob
 			end
@@ -446,9 +450,15 @@ class Registration
 				}.then do |iq|
 					next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
 
-					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
+					try_again
 				end
 			end
+
+			def try_again
+				CreditCard.for(
+					@customer, @tel, finish: @finish, maybe_bill: @maybe_bill
+				).then(&:write)
+			end
 		end
 
 		class InviteCode
@@ -537,11 +547,10 @@ class Registration
 			end
 
 			def form
-				price = @maybe_bill.bill? ? CONFIG[:activation_amount] + @tel.price : @tel.price
 				FormTemplate.render(
 					"registration/mail",
 					currency: @customer.currency,
-					price: price,
+					price: @maybe_bill.price(@tel),
 					**onboarding_extras
 				)
 			end
@@ -579,7 +588,9 @@ class Registration
 
 		def write
 			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
-				@finish.new(@customer, @tel).write
+				updated_customer =
+					@customer.with_balance(@customer.balance - @customer.monthly_price)
+				@finish.new(updated_customer, @tel).write
 			end
 		end
 	end
@@ -592,12 +603,42 @@ class Registration
 		end
 
 		def write
-			@tel.order(DB, @customer).then(
-				->(_) { customer_active_tel_purchased },
-				method(:number_purchase_error)
+			if @customer.balance >= @tel.price
+				@tel.order(DB, @customer).then(
+					->(_) { customer_active_tel_purchased },
+					method(:number_purchase_error)
+				)
+			else
+				buy_number.then {
+					try_again
+				}.then(&:write)
+			end
+		end
+
+		def try_again
+			reload_customer(@customer).then do |customer|
+				Finish.new(customer, @tel)
+			end
+		end
+
+		def form
+			FormTemplate.render(
+				"registration/buy_number",
+				tel: @tel
 			)
 		end
 
+		def buy_number
+			Command.reply { |reply|
+				reply.command << form
+			}.then { |iq|
+				Payment.for(
+					iq, @customer, @tel,
+					maybe_bill: ::Registration::Payment::JustCharge
+				).write
+			}
+		end
+
 	protected
 
 		def number_purchase_error(e)
@@ -646,7 +687,8 @@ class Registration
 				EMPromise.all([
 					TEL_SELECTIONS.delete(@customer.jid),
 					put_default_fwd,
-					use_referral_code
+					use_referral_code,
+					@tel.charge(@customer)
 				])
 			}.then do
 				FinishOnboarding.for(@customer, @tel).then(&:write)

test/test_registration.rb 🔗

@@ -1684,6 +1684,144 @@ class RegistrationTest < Minitest::Test
 		end
 		em :test_write_local_inventory
 
+		def test_write_local_inventory_must_pay
+			low_cust = customer(
+				sgx: @sgx,
+				jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+			).with_balance(5.0)
+			high_cust = customer(
+				sgx: @sgx,
+				jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+			).with_balance(100.0)
+
+			stub_request(
+				:post,
+				"https://dashboard.bandwidth.com/v1.0/accounts/moveto/moveTns"
+			).with(
+				body: {
+					CustomerOrderId: "test",
+					SourceAccountId: "bandwidth_account_id",
+					SiteId: "test_site",
+					SipPeerId: "test_peer",
+					TelephoneNumbers: { TelephoneNumber: "5555550000" }
+				}.to_xml(indent: 0, root: "MoveTnsOrder")
+			).to_return(status: 200, body: "", headers: {})
+
+			Registration::Finish::REDIS.expect(
+				:get,
+				nil,
+				["jmp_customer_pending_invite-test"]
+			)
+			Registration::Finish::REDIS.expect(
+				:del,
+				nil,
+				["jmp_customer_pending_invite-test"]
+			)
+			Registration::Finish::REDIS.expect(
+				:hget,
+				nil,
+				["jmp_group_codes", nil]
+			)
+			Registration::Finish::DB.expect(
+				:exec_defer,
+				EMPromise.resolve(OpenStruct.new(cmd_tuples: 1)),
+				[String, ["+15555550000"]]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:get,
+				EMPromise.resolve(nil),
+				["jmp_customer_backend_sgx-test"]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:set,
+				nil,
+				[
+					"catapult_fwd-+15555550000",
+					"xmpp:test\\40onboarding.example.com@proxy"
+				]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:set,
+				nil,
+				["catapult_fwd_timeout-customer_test@component", 25]
+			)
+
+			local_tel = TelSelections::ChooseTel::Tn::LocalInventory.new(
+				TelSelections::ChooseTel::Tn.new("+15555550000"),
+				"bandwidth_account_id",
+				price: 10.0
+			)
+
+			Registration::Payment::CreditCard.stub(
+				:new,
+				OpenStruct.new(
+					write: lambda {
+						# simulates successful try_again
+						Registration::Payment::CreditCard.for(
+							high_cust,
+							local_tel,
+							# we know maybe_bill will be passed as JustCharge
+							# since that's hardcoded
+							maybe_bill: Registration::Payment::JustCharge
+						)
+					}
+				)
+			) do
+				result = execute_command do
+					@sgx.expect(
+						:register!,
+						EMPromise.resolve(@sgx.with(
+							registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr|
+								ibr.phone = "+15555550000"
+							end
+						)),
+						["+15555550000"]
+					)
+
+					Command::COMMAND_MANAGER.expect(
+						:write,
+						EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+							iq.form.fields = [
+								{ var: "activation_method", value: "credit_card" },
+								{ var: "plan_name", value: "test_usd" }
+							]
+						}),
+						[Matching.new do |iq|
+							assert_equal :form, iq.form.type
+							assert iq.form.field("activation_method")
+							assert iq.form.field("plan_name")
+						end]
+					)
+
+					Command.execution.customer_repo.expect(
+						:find,
+						EMPromise.resolve(high_cust),
+						["test"]
+					)
+
+					Command::COMMAND_MANAGER.expect(
+						:write,
+						EMPromise.reject(:test_result),
+						[Matching.new do |iq|
+							assert_equal :form, iq.form.type
+							assert iq.form.field("subdomain")
+						end]
+					)
+
+					Registration::Finish.new(
+						low_cust,
+						local_tel
+					).write.catch { |e| e }
+				end
+				assert_equal :test_result, result
+				assert_mock @sgx
+				assert_mock Registration::Finish::REDIS
+				assert_mock Bwmsgsv2Repo::REDIS
+				assert_mock Command::COMMAND_MANAGER
+			end
+		end
+		em :test_write_local_inventory_must_pay
+
 		def test_write_tn_fail
 			create_order = stub_request(
 				:post,