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