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