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