diff --git a/lib/customer.rb b/lib/customer.rb index 05f3e22eb0e8c323fd9a3cec0d3eb7d49810a60b..83ef25007cf63d6abecdc8367840a8330b4e5f8d 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -24,7 +24,7 @@ class Customer def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan, :currency, :merchant_account, :plan_name, :minute_limit, :message_limit, :monthly_overage_limit, :activation_date, - :expires_at, :monthly_price, :save_plan! + :expires_at, :monthly_price, :save_plan!, :auto_top_up_amount def_delegators :@sgx, :deregister!, :register!, :registered?, :set_ogm_url, :fwd, :transcription_enabled def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage, @@ -85,15 +85,6 @@ class Customer EMPromise.resolve(self) end - 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 - else - @plan.auto_top_up_amount - end - end - def unused_invites InvitesRepo.new(DB).unused_invites(customer_id) end diff --git a/lib/low_balance.rb b/lib/low_balance.rb index dd4cdefde60983c2c776fbd00a8e5028a929d4a6..d237cfe66680def6ad93ab929ddcf62d55e9a141 100644 --- a/lib/low_balance.rb +++ b/lib/low_balance.rb @@ -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 |billing_customer| + for_no_lock(billing_customer, transaction_amount) + end end end - def self.for_no_lock(customer, auto: true) + def self.for_no_lock(customer, transaction_amount, auto: true) if auto && customer.auto_top_up_amount.positive? - AutoTopUp.for(customer) + 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,21 @@ 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? + return unless @transaction_amount > @customer.balance + + "You need an additional " \ + "$#{'%.2f' % (@transaction_amount - @customer.balance)} "\ + "to complete this transaction." + end + def btc_addresses_for_notification return if @btc_addresses.empty? @@ -48,13 +61,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,18 +80,24 @@ 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 + [ + (@target + @margin) - @customer.balance, + @customer.auto_top_up_amount + ].max + end + def sale - Transaction.sale( - @customer, - amount: @customer.auto_top_up_amount - ).then do |tx| + Transaction.sale(@customer, amount: top_up_amount).then do |tx| tx.insert.then { tx } end end @@ -91,7 +110,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 diff --git a/test/test_low_balance.rb b/test/test_low_balance.rb index f31380635ccf0f6fa6199f06b413cfb0e12e2fb9..7b1a8792f5ddae5999eecdd7746e142d7e9a9214 100644 --- a/test/test_low_balance.rb +++ b/test/test_low_balance.rb @@ -38,6 +38,43 @@ 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: 1), 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 +175,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, margin: 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, margin: 5) + + assert_equal 15, auto_top_up.top_up_amount + end + em :test_top_up_amount_when_target_less_than_expected_balance + + def test_negative_balance_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, margin: 5) + + assert_equal 51, auto_top_up.top_up_amount + end + em :test_negative_balance_target_less_than_expected_balance + def test_very_low_balance_notify! customer = Minitest::Mock.new(customer( balance: -100, @@ -150,7 +220,7 @@ class LowBalanceTest < Minitest::Test LowBalance::AutoTopUp::Transaction.expect( :sale, tx, - [customer], amount: 115 + [customer], amount: 110 ) auto_top_up.notify! assert_mock tx @@ -169,7 +239,7 @@ class LowBalanceTest < Minitest::Test LowBalance::AutoTopUp::Transaction.expect( :sale, tx, - [customer], amount: 26 + [customer], amount: 21 ) auto_top_up.notify! assert_mock tx