~singpolyma/sgx-jmp

48adae14cdd37c6b74b5b04bae279823fa85fd43 — Stephen Paul Weber 8 months ago 310c487
Try auto top up / low balance notify when not enough balance for a call

There is an edge case where a customer might not have auto-topped up yet but
they don't have enough balance for this call, so try to charge their card first
before telling them the call is a no go.
6 files changed, 116 insertions(+), 9 deletions(-)

M lib/call_attempt.rb
M lib/call_attempt_repo.rb
M lib/customer.rb
M test/test_helper.rb
M test/test_web.rb
M web.rb
M lib/call_attempt.rb => lib/call_attempt.rb +18 -4
@@ 2,6 2,8 @@

require "value_semantics/monkey_patched"

require_relative "low_balance"

class CallAttempt
	EXPENSIVE_ROUTE = {
		"usd_beta_unlimited-v20210223" => 0.9,


@@ 9,15 11,14 @@ class CallAttempt
	}.freeze

	def self.for(customer, other_tel, rate, usage, direction:, **kwargs)
		kwargs.merge!(direction: direction)
		included_credit = [customer.minute_limit.to_d - usage, 0].max
		if !rate || rate >= EXPENSIVE_ROUTE.fetch(customer.plan_name, 0.1)
			Unsupported.new(direction: direction)
		elsif included_credit + customer.balance < rate * 10
			NoBalance.new(balance: customer.balance, direction: direction)
			NoBalance.for(customer, other_tel, rate, usage, **kwargs)
		else
			for_ask_or_go(
				customer, other_tel, rate, usage, direction: direction, **kwargs
			)
			for_ask_or_go(customer, other_tel, rate, usage, **kwargs)
		end
	end



@@ 58,6 59,19 @@ class CallAttempt
	end

	class NoBalance
		def self.for(customer, other_tel, rate, usage, direction:, **kwargs)
			LowBalance.for(customer).then(&:notify!).then do |amount|
				if amount&.positive?
					CallAttempt.for(
						customer.with_balance(customer.balance + amount),
						other_tel, rate, usage, direction: direction, **kwargs
					)
				else
					NoBalance.new(balance: customer.balance, direction: direction)
				end
			end
		end

		value_semantics do
			balance Numeric
			direction Either(:inbound, :outbound)

M lib/call_attempt_repo.rb => lib/call_attempt_repo.rb +1 -0
@@ 1,6 1,7 @@
# frozen_string_literal: true

require "value_semantics/monkey_patched"
require "lazy_object"

require_relative "call_attempt"


M lib/customer.rb => lib/customer.rb +10 -4
@@ 45,13 45,19 @@ class Customer
		@sgx = sgx
	end

	def with_balance(balance)
		self.class.new(
			@customer_id, @jid,
			plan: @plan, balance: balance,
			tndetails: @tndetails, sgx: @sgx
		)
	end

	def with_plan(plan_name)
		self.class.new(
			@customer_id,
			@jid,
			@customer_id, @jid,
			plan: @plan.with_plan_name(plan_name),
			balance: @balance,
			sgx: @sgx
			balance: @balance, tndetails: @tndetails, sgx: @sgx
		)
	end


M test/test_helper.rb => test/test_helper.rb +1 -0
@@ 66,6 66,7 @@ CONFIG = {
		username: "test_bw_user",
		password: "test_bw_password"
	},
	notify_from: "notify_from@example.org",
	activation_amount: 1,
	plans: [
		{

M test/test_web.rb => test/test_web.rb +85 -1
@@ 2,11 2,15 @@

require "rack/test"
require "test_helper"
require "bwmsgsv2_repo"
require "customer_repo"
require_relative "../web"

ExpiringLock::REDIS = Minitest::Mock.new
Customer::BLATHER = Minitest::Mock.new
CustomerFwd::BANDWIDTH_VOICE = Minitest::Mock.new
Web::BANDWIDTH_VOICE = Minitest::Mock.new
LowBalance::AutoTopUp::Transaction = Minitest::Mock.new

class WebTest < Minitest::Test
	include Rack::Test::Methods


@@ 18,6 22,10 @@ class WebTest < Minitest::Test
				"catapult_jid-+15551234567" => "customer_customerid@component",
				"jmp_customer_jid-customerid_low" => "customer@example.com",
				"catapult_jid-+15551234560" => "customer_customerid_low@component",
				"jmp_customer_jid-customerid_topup" => "customer@example.com",
				"jmp_customer_auto_top_up_amount-customerid_topup" => "15",
				"jmp_customer_monthly_overage_limit-customerid_topup" => "99999",
				"catapult_jid-+15551234562" => "customer_customerid_topup@component",
				"jmp_customer_jid-customerid_limit" => "customer@example.com",
				"catapult_jid-+15551234561" => "customer_customerid_limit@component"
			),


@@ 32,6 40,11 @@ class WebTest < Minitest::Test
					"plan_name" => "test_usd",
					"expires_at" => Time.now + 100
				}],
				["customerid_topup"] => [{
					"balance" => BigDecimal("0.01"),
					"plan_name" => "test_usd",
					"expires_at" => Time.now + 100
				}],
				["customerid_limit"] => [{
					"balance" => BigDecimal(10),
					"plan_name" => "test_usd",


@@ 52,6 65,12 @@ class WebTest < Minitest::Test
						"customer_customerid@component" => IBR.new.tap do |ibr|
							ibr.phone = "+15551234567"
						end,
						"customer_customerid_low@component" => IBR.new.tap do |ibr|
							ibr.phone = "+15551234567"
						end,
						"customer_customerid_topup@component" => IBR.new.tap do |ibr|
							ibr.phone = "+15551234567"
						end,
						"customer_customerid_limit@component" => IBR.new.tap do |ibr|
							ibr.phone = "+15551234567"
						end


@@ 64,7 83,8 @@ class WebTest < Minitest::Test
				["test_usd", "+15557654321", :outbound] => [{ "rate" => 0.01 }],
				["test_usd", "+15557654321", :inbound] => [{ "rate" => 0.01 }],
				["customerid_limit"] => [{ "a" => -1000 }],
				["customerid_low"] => [{ "a" => -1000 }]
				["customerid_low"] => [{ "a" => -1000 }],
				["customerid_topup"] => [{ "a" => -1000 }]
			)
		)
		Web.opts[:common_logger] = FakeLog.new


@@ 94,6 114,12 @@ class WebTest < Minitest::Test
	em :test_outbound_forwards

	def test_outbound_low_balance
		ExpiringLock::REDIS.expect(
			:exists,
			EMPromise.resolve(1),
			["jmp_customer_low_balance-customerid_low"]
		)

		post(
			"/outbound/calls",
			{


@@ 111,9 137,60 @@ class WebTest < Minitest::Test
			"complete this call.</SpeakSentence></Response>",
			last_response.body
		)
		assert_mock ExpiringLock::REDIS
	end
	em :test_outbound_low_balance

	def test_outbound_low_balance_top_up
		LowBalance::AutoTopUp::Transaction.expect(
			:sale,
			EMPromise.resolve(
				OpenStruct.new(insert: EMPromise.resolve(nil), total: 15)
			),
			[Customer, { amount: 15 }]
		)

		ExpiringLock::REDIS.expect(
			:exists,
			nil,
			["jmp_customer_low_balance-customerid_topup"]
		)

		ExpiringLock::REDIS.expect(
			:setex,
			nil,
			["jmp_customer_low_balance-customerid_topup", Integer, Time]
		)

		Customer::BLATHER.expect(
			:<<,
			nil,
			[Blather::Stanza]
		)

		post(
			"/outbound/calls",
			{
				from: "customerid_topup",
				to: "+15557654321",
				callId: "acall"
			}.to_json,
			{ "CONTENT_TYPE" => "application/json" }
		)

		assert last_response.ok?
		assert_equal(
			"<?xml version=\"1.0\" encoding=\"utf-8\" ?><Response>" \
			"<Forward from=\"+15551234567\" to=\"+15557654321\" />" \
			"</Response>",
			last_response.body
		)
		assert_mock ExpiringLock::REDIS
		assert_mock Customer::BLATHER
		assert_mock LowBalance::AutoTopUp::Transaction
	end
	em :test_outbound_low_balance_top_up

	def test_outbound_unsupported
		post(
			"/outbound/calls",


@@ 213,6 290,12 @@ class WebTest < Minitest::Test
	em :test_inbound

	def test_inbound_low
		ExpiringLock::REDIS.expect(
			:exists,
			EMPromise.resolve(1),
			["jmp_customer_low_balance-customerid_low"]
		)

		post(
			"/inbound/calls",
			{


@@ 231,6 314,7 @@ class WebTest < Minitest::Test
			last_response.body
		)
		assert_mock CustomerFwd::BANDWIDTH_VOICE
		assert_mock ExpiringLock::REDIS
	end
	em :test_inbound_low


M web.rb => web.rb +1 -0
@@ 10,6 10,7 @@ require "sentry-ruby"

require_relative "lib/call_attempt_repo"
require_relative "lib/cdr"
require_relative "lib/oob"
require_relative "lib/roda_capture"
require_relative "lib/roda_em_promise"
require_relative "lib/rack_fiber"