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 verify(iq.form.field("code")&.value&.to_s).then {
438 Finish.new(@customer, @tel)
439 }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
440 end
441
442 protected
443
444 def invalid_code(e)
445 InviteCode.new(@customer, @tel, error: e.message)
446 end
447
448 def customer_id
449 @customer.customer_id
450 end
451
452 def verify(code)
453 InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) do
454 @customer.activate_plan_starting_now
455 end
456 end
457 end
458
459 class Mail
460 Payment.kinds[:mail] = method(:new)
461
462 def initialize(customer, tel, final_message: nil, **)
463 @customer = customer
464 @tel = tel
465 @final_message = final_message
466 end
467
468 def form
469 FormTemplate.render(
470 "registration/mail",
471 currency: @customer.currency,
472 final_message: @final_message,
473 **onboarding_extras
474 )
475 end
476
477 def onboarding_extras
478 jid = ProxiedJID.new(@customer.jid).unproxied
479 return {} unless jid.domain == CONFIG[:onboarding_domain]
480
481 {
482 customer_id: @customer.customer_id,
483 in_note: "Customer ID"
484 }
485 end
486
487 def write
488 Command.reply { |reply|
489 reply.allowed_actions = [:prev]
490 reply.status = :canceled
491 reply.command << form
492 }.then { |iq|
493 raise "Action not allowed" unless iq.prev?
494
495 Activation.for(@customer, nil, @tel).then(&:write)
496 }
497 end
498 end
499 end
500
501 class BillPlan
502 def initialize(customer, tel, finish: Finish)
503 @customer = customer
504 @tel = tel
505 @finish = finish
506 end
507
508 def write
509 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
510 @finish.new(@customer, @tel).write
511 end
512 end
513 end
514
515 class Finish
516 def initialize(customer, tel)
517 @customer = customer
518 @tel = tel
519 end
520
521 def write
522 BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
523 BandwidthTNOrder.create(
524 @tel,
525 customer_order_id: @customer.customer_id,
526 reservation_id: rid
527 ).then(&:poll).then(
528 ->(_) { customer_active_tel_purchased },
529 method(:number_purchase_error)
530 )
531 end
532 end
533
534 protected
535
536 def number_purchase_error(e)
537 Command.log.error "number_purchase_error", e
538 TEL_SELECTIONS.delete(@customer.jid).then {
539 TelSelections::ChooseTel.new.choose_tel(
540 error: "The JMP number #{@tel} is no longer available."
541 )
542 }.then { |tel| Finish.new(@customer, tel).write }
543 end
544
545 def raise_setup_error(e)
546 Command.log.error "@customer.register! failed", e
547 Command.finish(
548 "There was an error setting up your number, " \
549 "please contact JMP support.",
550 type: :error
551 )
552 end
553
554 def put_default_fwd
555 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
556 uri: "xmpp:#{@customer.jid}",
557 voicemail_enabled: true
558 ))
559 end
560
561 def customer_active_tel_purchased
562 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
563 EMPromise.all([
564 REDIS.del("pending_tel_for-#{@customer.jid}"),
565 put_default_fwd
566 ])
567 }.then do
568 FinishOnboarding.for(@customer, @tel).then(&:write)
569 end
570 end
571 end
572
573 module FinishOnboarding
574 def self.for(customer, tel, db: LazyObject.new { DB })
575 jid = ProxiedJID.new(customer.jid).unproxied
576 if jid.domain == CONFIG[:onboarding_domain]
577 Snikket.for(customer, tel, db: db)
578 else
579 NotOnboarding.new(customer, tel)
580 end
581 end
582
583 class Snikket
584 def self.for(customer, tel, db:)
585 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
586 if is.empty?
587 new(customer, tel, db: db)
588 else
589 GetInvite.for(is[0])
590 end
591 end
592 end
593
594 def initialize(customer, tel, error: nil, db:)
595 @customer = customer
596 @tel = tel
597 @error = error
598 @db = db
599 end
600
601 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
602
603 def write
604 Command.reply { |reply|
605 reply.allowed_actions = [:next]
606 reply.command << form
607 }.then do |iq|
608 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
609 CustomDomain.new(@tel).write
610 else
611 launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
612 end
613 end
614 end
615
616 def form
617 FormTemplate.render(
618 "registration/snikket",
619 tel: @tel,
620 error: @error
621 )
622 end
623
624 def launch(domain)
625 IQ_MANAGER.write(::Snikket::Launch.new(
626 nil, CONFIG[:snikket_hosting_api], domain: domain
627 )).then { |launched|
628 save_instance_and_wait(domain, launched)
629 }.catch { |e|
630 next EMPromise.reject(e) unless e.respond_to?(:text)
631
632 Snikket.new(@customer, @tel, error: e.text, db: @db).write
633 }
634 end
635
636 def save_instance_and_wait(domain, launched)
637 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
638 ::Snikket::Repo.new(db: @db).put(instance).then do
639 GetInvite.for(instance).then(&:write)
640 end
641 end
642
643 class GetInvite
644 def self.for(instance)
645 instance.fetch_invite.then do |xmpp_uri|
646 if xmpp_uri
647 GoToInvite.new(xmpp_uri)
648 else
649 new(instance)
650 end
651 end
652 end
653
654 def initialize(instance)
655 @instance = instance
656 end
657
658 def write
659 Command.reply { |reply|
660 reply.allowed_actions = [:next]
661 reply.command << FormTemplate.render(
662 "registration/snikket_wait",
663 domain: @instance.domain
664 )
665 }.then { GetInvite.for(@instance).then(&:write) }
666 end
667 end
668
669 class GoToInvite
670 def initialize(xmpp_uri)
671 @xmpp_uri = xmpp_uri
672 end
673
674 def write
675 Command.finish do |reply|
676 oob = OOB.find_or_create(reply.command)
677 oob.url = @xmpp_uri
678 end
679 end
680 end
681 end
682
683 class CustomDomain
684 def initialize(tel)
685 @tel = tel
686 end
687
688 CONTACT_SUPPORT =
689 "Please contact JMP support to set up " \
690 "an instance on an existing domain."
691
692 def write
693 Command.reply { |reply|
694 reply.allowed_actions = [:prev]
695 reply.status = :canceled
696 reply.note_type = :info
697 reply.note_text = CONTACT_SUPPORT
698 }.then do |iq|
699 raise "Action not allowed" unless iq.prev?
700
701 Snikket.new(@tel).write
702 end
703 end
704 end
705
706 class NotOnboarding
707 def initialize(customer, tel)
708 @customer = customer
709 @tel = tel
710 end
711
712 def write
713 WelcomeMessage.new(@customer, @tel).welcome
714 Command.finish("Your JMP account has been activated as #{@tel}")
715 end
716 end
717 end
718end