low_balance.rb

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