1# frozen_string_literal: true
2
3require "erb"
4require "ruby-bandwidth-iris"
5require "securerandom"
6
7require_relative "./alt_top_up_form"
8require_relative "./command"
9require_relative "./bandwidth_tn_order"
10require_relative "./em"
11require_relative "./oob"
12require_relative "./tel_selections"
13
14class Registration
15 def self.for(customer, tel_selections)
16 if (reg = customer.registered?)
17 Registered.new(reg.phone)
18 else
19 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
20 Activation.for(customer, tel)
21 end
22 end
23 end
24
25 class Registered
26 def initialize(tel)
27 @tel = tel
28 end
29
30 def write
31 Command.finish("You are already registered with JMP number #{@tel}")
32 end
33 end
34
35 class Activation
36 def self.for(customer, tel)
37 if customer.active?
38 Finish.new(customer, tel)
39 else
40 EMPromise.resolve(new(customer, tel))
41 end
42 end
43
44 def initialize(customer, tel)
45 @customer = customer
46 @tel = tel
47 end
48
49 attr_reader :customer, :tel
50
51 def form(center)
52 FormTemplate.render(
53 "registration/activate",
54 tel: tel,
55 rate_center: center
56 )
57 end
58
59 def write
60 rate_center.then { |center|
61 Command.reply do |reply|
62 reply.allowed_actions = [:next]
63 reply.command << form(center)
64 end
65 }.then(&method(:next_step))
66 end
67
68 def next_step(iq)
69 EMPromise.resolve(nil).then {
70 Payment.for(iq, customer, tel)
71 }.then(&:write)
72 end
73
74 protected
75
76 def rate_center
77 EM.promise_fiber {
78 center = BandwidthIris::Tn.get(tel).get_rate_center
79 "#{center[:rate_center]}, #{center[:state]}"
80 }.catch { nil }
81 end
82 end
83
84 module Payment
85 def self.kinds
86 @kinds ||= {}
87 end
88
89 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
90 plan_name = iq.form.field("plan_name").value.to_s
91 customer = customer.with_plan(plan_name)
92 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
93 raise "Invalid activation method"
94 }.call(customer, tel, final_message: final_message, finish: finish)
95 end
96
97 class Bitcoin
98 Payment.kinds[:bitcoin] = method(:new)
99
100 THIRTY_DAYS = 60 * 60 * 24 * 30
101
102 def initialize(customer, tel, final_message: nil, **)
103 @customer = customer
104 @customer_id = customer.customer_id
105 @tel = tel
106 @final_message = final_message
107 end
108
109 attr_reader :customer_id, :tel
110
111 def save
112 EMPromise.all([
113 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel),
114 REDIS.setex(
115 "pending_plan_for-#{customer_id}",
116 THIRTY_DAYS,
117 @customer.plan_name
118 )
119 ])
120 end
121
122 def note_text(amount, addr)
123 <<~NOTE
124 Activate your account by sending at least #{'%.6f' % amount} BTC to
125 #{addr}
126
127 You will receive a notification when your payment is complete.
128 NOTE
129 end
130
131 def write
132 EMPromise.all([
133 addr,
134 save,
135 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
136 ]).then do |(addr, _, rate)|
137 min = CONFIG[:activation_amount] / rate
138 Command.finish(
139 note_text(min, addr) + @final_message.to_s, status: :canceled
140 )
141 end
142 end
143
144 protected
145
146 def addr
147 @addr ||= @customer.btc_addresses.then { |addrs|
148 addrs.first || @customer.add_btc_address
149 }
150 end
151 end
152
153 class CreditCard
154 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
155
156 def self.for(customer, tel, finish: Finish, **)
157 customer.payment_methods.then do |payment_methods|
158 if (method = payment_methods.default_payment_method)
159 Activate.new(customer, method, tel, finish: finish)
160 else
161 new(customer, tel, finish: finish)
162 end
163 end
164 end
165
166 def initialize(customer, tel, finish: Finish)
167 @customer = customer
168 @tel = tel
169 @finish = finish
170 end
171
172 def oob(reply)
173 oob = OOB.find_or_create(reply.command)
174 oob.url = CONFIG[:credit_card_url].call(
175 reply.to.stripped.to_s.gsub("\\", "%5C"),
176 @customer.customer_id
177 )
178 oob.desc = "Add credit card, then return here to continue"
179 oob
180 end
181
182 def write
183 Command.reply { |reply|
184 reply.allowed_actions = [:next]
185 reply.note_type = :info
186 reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}"
187 }.then do
188 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
189 end
190 end
191
192 class Activate
193 def initialize(customer, payment_method, tel, finish: Finish)
194 @customer = customer
195 @payment_method = payment_method
196 @tel = tel
197 @finish = finish
198 end
199
200 def write
201 Transaction.sale(
202 @customer,
203 amount: CONFIG[:activation_amount],
204 payment_method: @payment_method
205 ).then(
206 method(:sold),
207 ->(_) { declined }
208 )
209 end
210
211 protected
212
213 def sold(tx)
214 tx.insert.then {
215 @customer.bill_plan
216 }.then do
217 @finish.new(@customer, @tel).write
218 end
219 end
220
221 DECLINE_MESSAGE =
222 "Your bank declined the transaction. " \
223 "Often this happens when a person's credit card " \
224 "is a US card that does not support international " \
225 "transactions, as JMP is not based in the USA, though " \
226 "we do support transactions in USD.\n\n" \
227 "If you were trying a prepaid card, you may wish to use "\
228 "Privacy.com instead, as they do support international " \
229 "transactions.\n\n " \
230 "You may add another card and then return here"
231
232 def decline_oob(reply)
233 oob = OOB.find_or_create(reply.command)
234 oob.url = CONFIG[:credit_card_url].call(
235 reply.to.stripped.to_s.gsub("\\", "%5C"),
236 @customer.customer_id
237 )
238 oob.desc = DECLINE_MESSAGE
239 oob
240 end
241
242 def declined
243 Command.reply { |reply|
244 reply_oob = decline_oob(reply)
245 reply.allowed_actions = [:next]
246 reply.note_type = :error
247 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
248 }.then do
249 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
250 end
251 end
252 end
253 end
254
255 class InviteCode
256 Payment.kinds[:code] = method(:new)
257
258 class Invalid < StandardError; end
259
260 FIELDS = [{
261 var: "code",
262 type: "text-single",
263 label: "Your invite code",
264 required: true
265 }].freeze
266
267 def initialize(customer, tel, error: nil, **)
268 @customer = customer
269 @tel = tel
270 @error = error
271 end
272
273 def add_form(reply)
274 form = reply.form
275 form.type = :form
276 form.title = "Enter Invite Code"
277 form.instructions = @error if @error
278 form.fields = FIELDS
279 end
280
281 def write
282 Command.reply { |reply|
283 reply.allowed_actions = [:next]
284 add_form(reply)
285 }.then(&method(:parse))
286 end
287
288 def parse(iq)
289 guard_too_many_tries.then {
290 verify(iq.form.field("code")&.value&.to_s)
291 }.then {
292 Finish.new(@customer, @tel)
293 }.catch_only(Invalid, &method(:invalid_code)).then(&:write)
294 end
295
296 protected
297
298 def guard_too_many_tries
299 REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
300 raise Invalid, "Too many wrong attempts" if t.to_i > 10
301 end
302 end
303
304 def invalid_code(e)
305 EMPromise.all([
306 REDIS.incr("jmp_invite_tries-#{customer_id}").then do
307 REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
308 end,
309 InviteCode.new(@customer, @tel, error: e.message)
310 ]).then(&:last)
311 end
312
313 def customer_id
314 @customer.customer_id
315 end
316
317 def verify(code)
318 EMPromise.resolve(nil).then do
319 DB.transaction do
320 valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
321 UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
322 WHERE code=$2 AND used_by_id IS NULL
323 SQL
324 raise Invalid, "Not a valid invite code: #{code}" unless valid
325
326 @customer.activate_plan_starting_now
327 end
328 end
329 end
330 end
331
332 class Mail
333 Payment.kinds[:mail] = method(:new)
334
335 def initialize(_customer, _tel, final_message: nil, **)
336 @final_message = final_message
337 end
338
339 def form
340 form = Blather::Stanza::X.new(:result)
341 form.title = "Activate by Mail or eTransfer"
342 form.instructions =
343 "Activate your account by sending at least " \
344 "$#{CONFIG[:activation_amount]}\nWe support payment by " \
345 "postal mail or, in Canada, by Interac eTransfer.\n\n" \
346 "You will receive a notification when your payment is complete." \
347 "#{@final_message}"
348
349 form.fields = fields.to_a
350 form
351 end
352
353 def fields
354 [
355 AltTopUpForm::MAILING_ADDRESS,
356 AltTopUpForm::IS_CAD
357 ].flatten
358 end
359
360 def write
361 Command.finish(status: :canceled) do |reply|
362 reply.command << form
363 end
364 end
365 end
366 end
367
368 class Finish
369 def initialize(customer, tel)
370 @customer = customer
371 @tel = tel
372 end
373
374 def write
375 BandwidthTNOrder.create(@tel).then(&:poll).then(
376 ->(_) { customer_active_tel_purchased },
377 ->(_) { number_purchase_error }
378 )
379 end
380
381 protected
382
383 def number_purchase_error
384 TEL_SELECTIONS.delete(@customer.jid).then {
385 TelSelections::ChooseTel.new.choose_tel(
386 error: "The JMP number #{@tel} is no longer available."
387 )
388 }.then { |tel| Finish.new(@customer, tel).write }
389 end
390
391 def raise_setup_error(e)
392 Command.log.error "@customer.register! failed", e
393 Command.finish(
394 "There was an error setting up your number, " \
395 "please contact JMP support.",
396 type: :error
397 )
398 end
399
400 def customer_active_tel_purchased
401 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
402 EMPromise.all([
403 REDIS.del("pending_tel_for-#{@customer.jid}"),
404 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
405 uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
406 ))
407 ])
408 }.then do
409 Command.finish("Your JMP account has been activated as #{@tel}")
410 end
411 end
412 end
413end