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