low_balance.rb

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