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