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 |customer|
 15				self.for_no_lock(customer, transaction_amount)
 16			end
 17		end
 18	end
 19
 20	def self.for_no_lock(customer, transaction_amount=0, auto: true)
 21		if auto && (customer.auto_top_up_amount.positive? || transaction_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? && @transaction_amount > @customer.balance)
 49
 50		"\nYou tried to perform an activity that cost #{@transaction_amount}"\
 51		"You need an additional #{'%.4f' % (@transaction_amount - @customer.balance)} "\
 52		"to perform this activity."
 53	end
 54
 55	def btc_addresses_for_notification
 56		return if @btc_addresses.empty?
 57
 58		"\nYou can buy credit by sending any amount of Bitcoin to one of " \
 59		"these addresses:\n#{@btc_addresses.join("\n")}"
 60	end
 61
 62	class AutoTopUp
 63		def self.for(customer, target=0)
 64			customer.payment_methods.then(&:default_payment_method).then do |method|
 65				blocked?(method).then do |block|
 66					next AutoTopUp.new(customer, method, target) if block.zero?
 67
 68					log.info("#{customer.customer_id} auto top up blocked")
 69					LowBalance.for_no_lock(customer, target, auto: false)
 70				end
 71			end
 72		end
 73
 74		def self.blocked?(method)
 75			return EMPromise.resolve(1) if method.nil?
 76
 77			REDIS.exists(
 78				"jmp_auto_top_up_block-#{method&.unique_number_identifier}"
 79			)
 80		end
 81
 82
 83
 84		def initialize(customer, method=nil, target=0, margin=10)
 85			@customer = customer
 86			@method = method
 87			@target = target
 88			@margin = margin
 89			@message = Blather::Stanza::Message.new
 90			@message.from = CONFIG[:notify_from]
 91		end
 92
 93		def top_up_amount()
 94			expected_balance = @customer.balance + @customer.auto_top_up_amount # @cutomer.auto_top_up is the adjusted auto_to_up
 95			if expected_balance < @target
 96				deficit = @target - expected_balance
 97				@customer.auto_top_up_amount + deficit + @margin
 98			else
 99				@customer.auto_top_up_amount
100			end
101		end
102
103		def sale
104			Transaction.sale(
105				@customer,
106				amount: top_up_amount()
107			).then do |tx|
108				tx.insert.then { tx }
109			end
110		end
111
112		def failed(e)
113			@method && REDIS.setex(
114				"jmp_auto_top_up_block-#{@method.unique_number_identifier}",
115				60 * 60 * 24 * 30,
116				Time.now
117			)
118			@message.body =
119				"Automatic top-up transaction for " \
120				"$#{top_up_amount()} failed: #{e.message}"
121			0
122		end
123
124		def notify!
125			sale.then { |tx|
126				@message.body =
127					"Automatic top-up has charged your default " \
128					"payment method and added #{tx} to your balance."
129				tx.total
130			}.catch(&method(:failed)).then { |amount|
131				@customer.stanza_to(@message)
132				amount
133			}
134		end
135	end
136
137	class Locked
138		def notify!
139			EMPromise.resolve(0)
140		end
141	end
142end