~singpolyma/sgx-jmp

1a2640d6f9f1f1ea8cbbfc0c7b4c05e57b3b242e — Osakpolor Obaseki 2 months ago 07575ad
Add low balance/auto top up with target amount
3 files changed, 114 insertions(+), 15 deletions(-)

M lib/customer.rb
M lib/low_balance.rb
M test/test_low_balance.rb
M lib/customer.rb => lib/customer.rb +4 -2
@@ 33,6 33,8 @@ class Customer
	               :add_btc_address, :declines, :mark_decline,
	               :transactions

	TOP_UP_ADJUSTEMENT_THRESHOLD = 5

	def self.extract(customer_id, jid, **kwargs)
		klass, *keys = if kwargs[:parent_customer_id]
			[ChildCustomer, :parent_customer_id]


@@ 87,8 89,8 @@ class Customer

	def auto_top_up_amount
		if @plan.auto_top_up_amount.positive? &&
		   balance < -@plan.auto_top_up_amount + 5
			-balance + @plan.auto_top_up_amount
		   balance + @plan.auto_top_up_amount < TOP_UP_ADJUSTEMENT_THRESHOLD
			-balance + @plan.auto_top_up_amount # amount needed to set balance to @plan.auto_top_up_amount
		else
			@plan.auto_top_up_amount
		end

M lib/low_balance.rb => lib/low_balance.rb +39 -13
@@ 4,30 4,33 @@ require_relative "expiring_lock"
require_relative "transaction"

class LowBalance
	def self.for(customer)
	def self.for(customer, transaction_amount=0)
		return Locked.new unless customer.registered?

		ExpiringLock.new(
			"jmp_customer_low_balance-#{customer.billing_customer_id}",
			expiry: 60 * 60 * 24 * 7
		).with(-> { Locked.new }) do
			customer.billing_customer.then(&method(:for_no_lock))
			customer.billing_customer.then do |customer|
				self.for_no_lock(customer, transaction_amount)
			end
		end
	end

	def self.for_no_lock(customer, auto: true)
		if auto && customer.auto_top_up_amount.positive?
			AutoTopUp.for(customer)
	def self.for_no_lock(customer, transaction_amount=0, auto: true)
		if auto && (customer.auto_top_up_amount.positive? || transaction_amount.positive?)
			AutoTopUp.for(customer, transaction_amount)
		else
			customer.btc_addresses.then do |btc_addresses|
				new(customer, btc_addresses)
				new(customer, btc_addresses, transaction_amount)
			end
		end
	end

	def initialize(customer, btc_addresses)
	def initialize(customer, btc_addresses, transaction_amount=0)
		@customer = customer
		@btc_addresses = btc_addresses
		@transaction_amount = transaction_amount
	end

	def notify!


@@ 35,11 38,20 @@ class LowBalance
		m.from = CONFIG[:notify_from]
		m.body =
			"Your balance of $#{'%.4f' % @customer.balance} is low." \
			"#{pending_cost_for_notification}" \
			"#{btc_addresses_for_notification}"
		@customer.stanza_to(m)
		EMPromise.resolve(0)
	end

	def pending_cost_for_notification
		return unless (@transaction_amount.positive? && @transaction_amount > @customer.balance)

		"\nYou tried to perform an activity that cost #{@transaction_amount}"\
		"You need an additional #{'%.4f' % (@transaction_amount - @customer.balance)} "\
		"to perform this activity."
	end

	def btc_addresses_for_notification
		return if @btc_addresses.empty?



@@ 48,13 60,13 @@ class LowBalance
	end

	class AutoTopUp
		def self.for(customer)
		def self.for(customer, target=0)
			customer.payment_methods.then(&:default_payment_method).then do |method|
				blocked?(method).then do |block|
					next AutoTopUp.new(customer, method) if block.zero?
					next AutoTopUp.new(customer, method, target) if block.zero?

					log.info("#{customer.customer_id} auto top up blocked")
					LowBalance.for_no_lock(customer, auto: false)
					LowBalance.for_no_lock(customer, target, auto: false)
				end
			end
		end


@@ 67,17 79,31 @@ class LowBalance
			)
		end

		def initialize(customer, method=nil)


		def initialize(customer, method=nil, target=0, margin=10)
			@customer = customer
			@method = method
			@target = target
			@margin = margin
			@message = Blather::Stanza::Message.new
			@message.from = CONFIG[:notify_from]
		end

		def top_up_amount()
			expected_balance = @customer.balance + @customer.auto_top_up_amount # @cutomer.auto_top_up is the adjusted auto_to_up
			if expected_balance < @target
				deficit = @target - expected_balance
				@customer.auto_top_up_amount + deficit + @margin
			else
				@customer.auto_top_up_amount
			end
		end

		def sale
			Transaction.sale(
				@customer,
				amount: @customer.auto_top_up_amount
				amount: top_up_amount()
			).then do |tx|
				tx.insert.then { tx }
			end


@@ 91,7 117,7 @@ class LowBalance
			)
			@message.body =
				"Automatic top-up transaction for " \
				"$#{@customer.auto_top_up_amount} failed: #{e.message}"
				"$#{top_up_amount()} failed: #{e.message}"
			0
		end


M test/test_low_balance.rb => test/test_low_balance.rb +71 -0
@@ 38,6 38,44 @@ class LowBalanceTest < Minitest::Test
	end
	em :test_for_no_auto_top_up

	def test_for_auto_top_up_on_transaction_amount
		ExpiringLock::REDIS.expect(
			:set,
			EMPromise.resolve("OK"),
			["jmp_customer_low_balance-test", Time, "EX", 604800, "NX"]
		)
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["block_credit_cards"]
		)
		LowBalance::AutoTopUp::REDIS.expect(
			:exists,
			0,
			["jmp_auto_top_up_block-abcd"]
		)
		braintree_customer = Minitest::Mock.new
		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
		payment_methods = OpenStruct.new(payment_methods: [
			OpenStruct.new(default?: true, unique_number_identifier: "abcd")
		])
		braintree_customer.expect(
			:find,
			EMPromise.resolve(payment_methods),
			["test"]
		)
		assert_kind_of(
			LowBalance::AutoTopUp,
			LowBalance.for(customer(auto_top_up_amount: 0), transaction_amount = 15).sync
		)
		assert_mock ExpiringLock::REDIS
		assert_mock CustomerFinancials::REDIS
		assert_mock CustomerFinancials::BRAINTREE
		assert_mock braintree_customer
	end
	em :test_for_auto_top_up_on_transaction_amount


	def test_for_auto_top_up
		ExpiringLock::REDIS.expect(
			:set,


@@ 138,6 176,39 @@ class LowBalanceTest < Minitest::Test
		end
		em :test_notify!

		def test_top_up_amount_when_target_greater_than_expected_balance
			customer = Minitest::Mock.new(customer(
				balance: 10,
				auto_top_up_amount: 15
			))
			auto_top_up = LowBalance::AutoTopUp.new(customer, nil, 30, 5)

			assert_equal 25, auto_top_up.top_up_amount()
		end
		em :test_top_up_amount_when_target_greater_than_expected_balance

		def test_top_up_amount_when_target_less_than_expected_balance
			customer = Minitest::Mock.new(customer(
				balance: 10,
				auto_top_up_amount: 15
			))
			auto_top_up = LowBalance::AutoTopUp.new(customer, nil, 12, 5)

			assert_equal 15, auto_top_up.top_up_amount()
		end
		em :test_top_up_amount_when_target_less_than_expected_balance

		def test_top_up_amount_when_balance_is_negative_and_target_less_than_expected_balance
			customer = Minitest::Mock.new(customer(
				balance: -11,
				auto_top_up_amount: 15
			))
			auto_top_up = LowBalance::AutoTopUp.new(customer, nil, 35, 5)

			assert_equal 51, auto_top_up.top_up_amount()
		end
		em :test_top_up_amount_when_balance_is_negative_and_target_less_than_expected_balance

		def test_very_low_balance_notify!
			customer = Minitest::Mock.new(customer(
				balance: -100,