low_balance.rb

  1# frozen_string_literal: true
  2
  3require_relative "expiring_lock"
  4require_relative "transaction"
  5
  6class LowBalance
  7	def self.for(customer, transaction_amount=0)
  8		return Locked.new unless customer.registered?
  9
 10		ExpiringLock.new(
 11			"jmp_customer_low_balance-#{customer.billing_customer_id}",
 12			expiry: 60 * 60 * 24 * 7
 13		).with(-> { Locked.new }) do
 14			customer.billing_customer.then do |billing_customer|
 15				for_no_lock(billing_customer, transaction_amount)
 16			end
 17		end
 18	end
 19
 20	def self.for_no_lock(customer, transaction_amount, auto: true)
 21		if auto && customer.auto_top_up_amount.positive?
 22			AutoTopUp.for(customer, transaction_amount)
 23		else
 24			customer.btc_addresses.then do |btc_addresses|
 25				new(customer, btc_addresses, transaction_amount)
 26			end
 27		end
 28	end
 29
 30	def initialize(customer, btc_addresses, transaction_amount=0)
 31		@customer = customer
 32		@btc_addresses = btc_addresses
 33		@transaction_amount = transaction_amount
 34	end
 35
 36	def notify!
 37		m = Blather::Stanza::Message.new
 38		m.from = CONFIG[:notify_from]
 39		m.body =
 40			"Your balance of $#{'%.4f' % @customer.balance} is low." \
 41			"#{pending_cost_for_notification}" \
 42			"#{btc_addresses_for_notification}"
 43		@customer.stanza_to(m)
 44		EMPromise.resolve(0)
 45	end
 46
 47	def pending_cost_for_notification
 48		return unless @transaction_amount&.positive?
 49		return unless @transaction_amount > @customer.balance
 50
 51		"You need an additional " \
 52		"$#{'%.2f' % (@transaction_amount - @customer.balance)} "\
 53		"to complete this transaction."
 54	end
 55
 56	def btc_addresses_for_notification
 57		return if @btc_addresses.empty?
 58
 59		"\nYou can buy credit by sending any amount of Bitcoin to one of " \
 60		"these addresses:\n#{@btc_addresses.join("\n")}"
 61	end
 62
 63	class AutoTopUp
 64		def self.for(customer, target=0)
 65			customer.payment_methods.then(&:default_payment_method).then do |method|
 66				blocked?(method).then do |block|
 67					next AutoTopUp.new(customer, method, target) if block.zero?
 68
 69					log.info("#{customer.customer_id} auto top up blocked")
 70					LowBalance.for_no_lock(customer, target, auto: false)
 71				end
 72			end
 73		end
 74
 75		def self.blocked?(method)
 76			return EMPromise.resolve(1) if method.nil?
 77
 78			REDIS.exists(
 79				"jmp_auto_top_up_block-#{method&.unique_number_identifier}"
 80			)
 81		end
 82
 83		def initialize(customer, method=nil, target=0, margin: 10)
 84			@customer = customer
 85			@method = method
 86			@target = target
 87			@margin = margin
 88			@message = Blather::Stanza::Message.new
 89			@message.from = CONFIG[:notify_from]
 90		end
 91
 92		def top_up_amount
 93			[
 94				(@target + @margin) - @customer.balance,
 95				@customer.auto_top_up_amount
 96			].max
 97		end
 98
 99		def sale
100			Transaction.sale(@customer, amount: top_up_amount).then do |tx|
101				tx.insert.then { tx }
102			end
103		end
104
105		def failed(e)
106			@method && REDIS.setex(
107				"jmp_auto_top_up_block-#{@method.unique_number_identifier}",
108				60 * 60 * 24 * 30,
109				Time.now
110			)
111			@message.body =
112				"Automatic top-up transaction for " \
113				"$#{top_up_amount} failed: #{e.message}"
114			0
115		end
116
117		def notify!
118			sale.then { |tx|
119				@message.body =
120					"Automatic top-up has charged your default " \
121					"payment method and added #{tx} to your balance."
122				tx.total
123			}.catch(&method(:failed)).then { |amount|
124				@customer.stanza_to(@message)
125				amount
126			}
127		end
128	end
129
130	class Locked
131		def notify!
132			EMPromise.resolve(0)
133		end
134	end
135end