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 "./bandwidth_tn_reservation_repo"
10require_relative "./command"
11require_relative "./em"
12require_relative "./invites_repo"
13require_relative "./oob"
14require_relative "./proxied_jid"
15require_relative "./tel_selections"
16require_relative "./welcome_message"
17
18class Registration
19 def self.for(customer, tel_selections)
20 if (reg = customer.registered?)
21 Registered.for(customer, reg.phone)
22 else
23 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
24 BandwidthTnReservationRepo.new.ensure(customer, tel)
25 FinishOrStartActivation.for(customer, tel)
26 end
27 end
28 end
29
30 class Registered
31 def self.for(customer, tel)
32 jid = ProxiedJID.new(customer.jid).unproxied
33 if jid.domain == CONFIG[:onboarding_domain]
34 FinishOnboarding.for(customer, tel)
35 else
36 new(tel)
37 end
38 end
39
40 def initialize(tel)
41 @tel = tel
42 end
43
44 def write
45 Command.finish("You are already registered with JMP number #{@tel}")
46 end
47 end
48
49 class FinishOrStartActivation
50 def self.for(customer, tel)
51 if customer.active?
52 Finish.new(customer, tel)
53 elsif customer.balance >= CONFIG[:activation_amount_accept]
54 BillPlan.new(customer, tel)
55 else
56 new(customer, tel)
57 end
58 end
59
60 def initialize(customer, tel)
61 @customer = customer
62 @tel = tel
63 end
64
65 def write
66 Command.reply { |reply|
67 reply.allowed_actions = [:next]
68 reply.note_type = :info
69 reply.note_text = File.read("#{__dir__}/../fup.txt")
70 }.then { Activation.for(@customer, @tel).write }
71 end
72 end
73
74 class Activation
75 def self.for(customer, tel)
76 jid = ProxiedJID.new(customer.jid).unproxied
77 if CONFIG[:approved_domains].key?(jid.domain.to_sym)
78 Allow.for(customer, tel, jid)
79 else
80 new(customer, tel)
81 end
82 end
83
84 def initialize(customer, tel)
85 @customer = customer
86 @tel = tel
87 end
88
89 attr_reader :customer, :tel
90
91 def form(center)
92 FormTemplate.render(
93 "registration/activate",
94 tel: tel,
95 rate_center: center
96 )
97 end
98
99 def write
100 rate_center.then { |center|
101 Command.reply do |reply|
102 reply.allowed_actions = [:next]
103 reply.command << form(center)
104 end
105 }.then(&method(:next_step))
106 end
107
108 def next_step(iq)
109 EMPromise.resolve(nil).then {
110 Payment.for(iq, customer, tel)
111 }.then(&:write)
112 end
113
114 protected
115
116 def rate_center
117 EM.promise_fiber {
118 center = BandwidthIris::Tn.get(tel).get_rate_center
119 "#{center[:rate_center]}, #{center[:state]}"
120 }.catch { nil }
121 end
122
123 class Allow < Activation
124 def self.for(customer, tel, jid)
125 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
126 new(customer, tel, credit_to)
127 end
128
129 def initialize(customer, tel, credit_to)
130 super(customer, tel)
131 @credit_to = credit_to
132 end
133
134 def form(center)
135 FormTemplate.render(
136 "registration/allow",
137 tel: tel,
138 rate_center: center,
139 domain: customer.jid.domain
140 )
141 end
142
143 def next_step(iq)
144 plan_name = iq.form.field("plan_name").value.to_s
145 @customer = customer.with_plan(plan_name)
146 EMPromise.resolve(nil).then { activate }.then do
147 Finish.new(customer, tel).write
148 end
149 end
150
151 protected
152
153 def activate
154 DB.transaction do
155 if @credit_to
156 DB.exec(<<~SQL, [@credit_to, customer.customer_id])
157 INSERT INTO invites (creator_id, used_by_id, used_at)
158 VALUES ($1, $2, LOCALTIMESTAMP)
159 SQL
160 end
161 @customer.activate_plan_starting_now
162 end
163 end
164 end
165 end
166
167 module Payment
168 def self.kinds
169 @kinds ||= {}
170 end
171
172 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
173 plan_name = iq.form.field("plan_name").value.to_s
174 customer = customer.with_plan(plan_name)
175 customer.save_plan!.then do
176 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
177 raise "Invalid activation method"
178 }.call(customer, tel, final_message: final_message, finish: finish)
179 end
180 end
181
182 class Bitcoin
183 Payment.kinds[:bitcoin] = method(:new)
184
185 THIRTY_DAYS = 60 * 60 * 24 * 30
186
187 def initialize(customer, tel, final_message: nil, **)
188 @customer = customer
189 @customer_id = customer.customer_id
190 @tel = tel
191 @final_message = final_message
192 end
193
194 attr_reader :customer_id, :tel
195
196 def save
197 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
198 end
199
200 def form(rate, addr)
201 amount = CONFIG[:activation_amount] / rate
202
203 FormTemplate.render(
204 "registration/btc",
205 amount: amount,
206 addr: addr,
207 final_message: @final_message
208 )
209 end
210
211 def write
212 EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
213 Command.reply { |reply|
214 reply.allowed_actions = [:prev]
215 reply.status = :canceled
216 reply.command << form(rate, addr)
217 }.then(&method(:handle_possible_prev))
218 end
219 end
220
221 protected
222
223 def handle_possible_prev(iq)
224 raise "Action not allowed" unless iq.prev?
225
226 Activation.for(@customer, @tel).then(&:write)
227 end
228
229 def addr_and_rate
230 EMPromise.all([
231 @customer.btc_addresses.then { |addrs|
232 addrs.first || @customer.add_btc_address
233 },
234 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
235 ])
236 end
237 end
238
239 class CreditCard
240 Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
241
242 def self.for(in_customer, tel, finish: Finish, **)
243 reload_customer(in_customer).then do |(customer, payment_methods)|
244 if customer.balance >= CONFIG[:activation_amount_accept]
245 next BillPlan.new(customer, tel, finish: finish)
246 end
247
248 if (method = payment_methods.default_payment_method)
249 next Activate.new(customer, method, tel, finish: finish)
250 end
251
252 new(customer, tel, finish: finish)
253 end
254 end
255
256 def self.reload_customer(customer)
257 EMPromise.all([
258 Command.execution.customer_repo.find(customer.customer_id),
259 customer.payment_methods
260 ])
261 end
262
263 def initialize(customer, tel, finish: Finish)
264 @customer = customer
265 @tel = tel
266 @finish = finish
267 end
268
269 def oob(reply)
270 oob = OOB.find_or_create(reply.command)
271 oob.url = CONFIG[:credit_card_url].call(
272 reply.to.stripped.to_s.gsub("\\", "%5C"),
273 @customer.customer_id
274 ) + "&amount=#{CONFIG[:activation_amount]}"
275 oob.desc = "Add credit card, save, then next here to continue"
276 oob
277 end
278
279 def write
280 Command.reply { |reply|
281 reply.allowed_actions = [:next, :prev]
282 toob = oob(reply)
283 reply.note_type = :info
284 reply.note_text = "#{toob.desc}: #{toob.url}"
285 }.then do |iq|
286 next Activation.for(@customer, @tel).then(&:write) if iq.prev?
287
288 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
289 end
290 end
291
292 class Activate
293 def initialize(customer, payment_method, tel, finish: Finish)
294 @customer = customer
295 @payment_method = payment_method
296 @tel = tel
297 @finish = finish
298 end
299
300 def write
301 CreditCardSale.create(
302 @customer,
303 amount: CONFIG[:activation_amount],
304 payment_method: @payment_method
305 ).then(
306 ->(_) { sold },
307 ->(_) { declined }
308 )
309 end
310
311 protected
312
313 def sold
314 BillPlan.new(@customer, @tel, finish: @finish).write
315 end
316
317 DECLINE_MESSAGE =
318 "Your bank declined the transaction. " \
319 "Often this happens when a person's credit card " \
320 "is a US card that does not support international " \
321 "transactions, as JMP is not based in the USA, though " \
322 "we do support transactions in USD.\n\n" \
323 "You may add another card"
324
325 def decline_oob(reply)
326 oob = OOB.find_or_create(reply.command)
327 oob.url = CONFIG[:credit_card_url].call(
328 reply.to.stripped.to_s.gsub("\\", "%5C"),
329 @customer.customer_id
330 ) + "&amount=#{CONFIG[:activation_amount]}"
331 oob.desc = DECLINE_MESSAGE
332 oob
333 end
334
335 def declined
336 Command.reply { |reply|
337 reply_oob = decline_oob(reply)
338 reply.allowed_actions = [:next]
339 reply.note_type = :error
340 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
341 }.then do
342 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
343 end
344 end
345 end
346 end
347
348 class InviteCode
349 Payment.kinds[:code] = method(:new)
350
351 FIELDS = [{
352 var: "code",
353 type: "text-single",
354 label: "Your referral code",
355 required: true
356 }].freeze
357
358 def initialize(customer, tel, error: nil, **)
359 @customer = customer
360 @tel = tel
361 @error = error
362 end
363
364 def add_form(reply)
365 form = reply.form
366 form.type = :form
367 form.title = "Enter Referral Code"
368 form.instructions = @error if @error
369 form.fields = FIELDS
370 end
371
372 def write
373 Command.reply { |reply|
374 reply.allowed_actions = [:next, :prev]
375 add_form(reply)
376 }.then(&method(:parse))
377 end
378
379 def parse(iq)
380 return Activation.for(@customer, @tel).then(&:write) if iq.prev?
381
382 guard_too_many_tries.then {
383 verify(iq.form.field("code")&.value&.to_s)
384 }.then {
385 Finish.new(@customer, @tel)
386 }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
387 end
388
389 protected
390
391 def guard_too_many_tries
392 REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
393 raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10
394 end
395 end
396
397 def invalid_code(e)
398 EMPromise.all([
399 REDIS.incr("jmp_invite_tries-#{customer_id}").then do
400 REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
401 end,
402 InviteCode.new(@customer, @tel, error: e.message)
403 ]).then(&:last)
404 end
405
406 def customer_id
407 @customer.customer_id
408 end
409
410 def verify(code)
411 InvitesRepo.new(DB).claim_code(customer_id, code) do
412 @customer.activate_plan_starting_now
413 end
414 end
415 end
416
417 class Mail
418 Payment.kinds[:mail] = method(:new)
419
420 def initialize(customer, tel, final_message: nil, **)
421 @customer = customer
422 @tel = tel
423 @final_message = final_message
424 end
425
426 def form
427 FormTemplate.render(
428 "registration/mail",
429 currency: @customer.currency,
430 final_message: @final_message,
431 **onboarding_extras
432 )
433 end
434
435 def onboarding_extras
436 jid = ProxiedJID.new(@customer.jid).unproxied
437 return {} unless jid.domain == CONFIG[:onboarding_domain]
438
439 {
440 customer_id: @customer.customer_id,
441 in_note: "Customer ID"
442 }
443 end
444
445 def write
446 Command.reply { |reply|
447 reply.allowed_actions = [:prev]
448 reply.status = :canceled
449 reply.command << form
450 }.then { |iq|
451 raise "Action not allowed" unless iq.prev?
452
453 Activation.for(@customer, @tel).then(&:write)
454 }
455 end
456 end
457 end
458
459 class BillPlan
460 def initialize(customer, tel, finish: Finish)
461 @customer = customer
462 @tel = tel
463 @finish = finish
464 end
465
466 def write
467 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
468 @finish.new(@customer, @tel).write
469 end
470 end
471 end
472
473 class Finish
474 def initialize(customer, tel)
475 @customer = customer
476 @tel = tel
477 end
478
479 def write
480 BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
481 BandwidthTNOrder.create(
482 @tel,
483 customer_order_id: @customer.customer_id,
484 reservation_id: rid
485 ).then(&:poll).then(
486 ->(_) { customer_active_tel_purchased },
487 method(:number_purchase_error)
488 )
489 end
490 end
491
492 protected
493
494 def number_purchase_error(e)
495 Command.log.error "number_purchase_error", e
496 TEL_SELECTIONS.delete(@customer.jid).then {
497 TelSelections::ChooseTel.new.choose_tel(
498 error: "The JMP number #{@tel} is no longer available."
499 )
500 }.then { |tel| Finish.new(@customer, tel).write }
501 end
502
503 def raise_setup_error(e)
504 Command.log.error "@customer.register! failed", e
505 Command.finish(
506 "There was an error setting up your number, " \
507 "please contact JMP support.",
508 type: :error
509 )
510 end
511
512 def put_default_fwd
513 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
514 uri: "xmpp:#{@customer.jid}",
515 voicemail_enabled: true
516 ))
517 end
518
519 def customer_active_tel_purchased
520 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
521 EMPromise.all([
522 REDIS.del("pending_tel_for-#{@customer.jid}"),
523 put_default_fwd
524 ])
525 }.then do
526 FinishOnboarding.for(@customer, @tel).then(&:write)
527 end
528 end
529 end
530
531 module FinishOnboarding
532 def self.for(customer, tel, db: LazyObject.new { DB })
533 jid = ProxiedJID.new(customer.jid).unproxied
534 if jid.domain == CONFIG[:onboarding_domain]
535 Snikket.for(customer, tel, db: db)
536 else
537 NotOnboarding.new(customer, tel)
538 end
539 end
540
541 class Snikket
542 def self.for(customer, tel, db:)
543 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
544 if is.empty?
545 new(customer, tel, db: db)
546 else
547 GetInvite.for(is[0])
548 end
549 end
550 end
551
552 def initialize(customer, tel, error: nil, db:)
553 @customer = customer
554 @tel = tel
555 @error = error
556 @db = db
557 end
558
559 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
560
561 def write
562 Command.reply { |reply|
563 reply.allowed_actions = [:next]
564 reply.command << form
565 }.then do |iq|
566 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
567 CustomDomain.new(@tel).write
568 else
569 launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
570 end
571 end
572 end
573
574 def form
575 FormTemplate.render(
576 "registration/snikket",
577 tel: @tel,
578 error: @error
579 )
580 end
581
582 def launch(domain)
583 IQ_MANAGER.write(::Snikket::Launch.new(
584 nil, CONFIG[:snikket_hosting_api], domain: domain
585 )).then { |launched|
586 save_instance_and_wait(domain, launched)
587 }.catch { |e|
588 next EMPromise.reject(e) unless e.respond_to?(:text)
589
590 Snikket.new(@customer, @tel, error: e.text, db: @db).write
591 }
592 end
593
594 def save_instance_and_wait(domain, launched)
595 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
596 ::Snikket::Repo.new(db: @db).put(instance).then do
597 GetInvite.for(instance).then(&:write)
598 end
599 end
600
601 class GetInvite
602 def self.for(instance)
603 instance.fetch_invite.then do |xmpp_uri|
604 if xmpp_uri
605 GoToInvite.new(xmpp_uri)
606 else
607 new(instance)
608 end
609 end
610 end
611
612 def initialize(instance)
613 @instance = instance
614 end
615
616 def write
617 Command.reply { |reply|
618 reply.allowed_actions = [:next]
619 reply.command << FormTemplate.render(
620 "registration/snikket_wait",
621 domain: @instance.domain
622 )
623 }.then { GetInvite.for(@instance).then(&:write) }
624 end
625 end
626
627 class GoToInvite
628 def initialize(xmpp_uri)
629 @xmpp_uri = xmpp_uri
630 end
631
632 def write
633 Command.finish do |reply|
634 oob = OOB.find_or_create(reply.command)
635 oob.url = @xmpp_uri
636 end
637 end
638 end
639 end
640
641 class CustomDomain
642 def initialize(tel)
643 @tel = tel
644 end
645
646 CONTACT_SUPPORT =
647 "Please contact JMP support to set up " \
648 "an instance on an existing domain."
649
650 def write
651 Command.reply { |reply|
652 reply.allowed_actions = [:prev]
653 reply.status = :canceled
654 reply.note_type = :info
655 reply.note_text = CONTACT_SUPPORT
656 }.then do |iq|
657 raise "Action not allowed" unless iq.prev?
658
659 Snikket.new(@tel).write
660 end
661 end
662 end
663
664 class NotOnboarding
665 def initialize(customer, tel)
666 @customer = customer
667 @tel = tel
668 end
669
670 def write
671 WelcomeMessage.new(@customer, @tel).welcome
672 Command.finish("Your JMP account has been activated as #{@tel}")
673 end
674 end
675 end
676end