# frozen_string_literal: true require "erb" require "ruby-bandwidth-iris" require "securerandom" require_relative "./alt_top_up_form" require_relative "./bandwidth_tn_order" require_relative "./bandwidth_tn_reservation_repo" require_relative "./command" require_relative "./em" require_relative "./invites_repo" require_relative "./oob" require_relative "./parent_code_repo" require_relative "./proxied_jid" require_relative "./tel_selections" require_relative "./welcome_message" class Registration def self.for(customer, google_play_userid, tel_selections) if (reg = customer.registered?) Registered.for(customer, reg.phone) else tel_selections[customer.jid].then(&:choose_tel).then do |tel| BandwidthTnReservationRepo.new.ensure(customer, tel) FinishOrStartActivation.for(customer, google_play_userid, tel) end end end class Registered def self.for(customer, tel) jid = ProxiedJID.new(customer.jid).unproxied if jid.domain == CONFIG[:onboarding_domain] FinishOnboarding.for(customer, tel) else new(tel) end end def initialize(tel) @tel = tel end def write Command.finish("You are already registered with JMP number #{@tel}") end end class FinishOrStartActivation def self.for(customer, google_play_userid, tel) if customer.active? Finish.new(customer, tel) elsif customer.balance >= CONFIG[:activation_amount_accept] BillPlan.new(customer, tel) else new(customer, google_play_userid, tel) end end def initialize(customer, google_play_userid, tel) @customer = customer @tel = tel @google_play_userid = google_play_userid end def write Command.reply { |reply| reply.allowed_actions = [:next] reply.note_type = :info reply.note_text = File.read("#{__dir__}/../fup.txt") }.then { Activation.for(@customer, @google_play_userid, @tel).write } end end class Activation def self.for(customer, google_play_userid, tel) jid = ProxiedJID.new(customer.jid).unproxied if CONFIG[:approved_domains].key?(jid.domain.to_sym) Allow.for(customer, tel, jid) elsif google_play_userid GooglePlay.new(customer, google_play_userid, tel) else new(customer, tel) end end def initialize(customer, tel) @customer = customer @tel = tel @invites = InvitesRepo.new(DB, REDIS) end attr_reader :customer, :tel def form FormTemplate.render("registration/activate", tel: tel) end def write Command.reply { |reply| reply.allowed_actions = [:next] reply.command << form }.then(&method(:next_step)) end def next_step(iq) code = iq.form.field("code")&.value&.to_s save_customer_plan(iq, code).then { finish_if_valid_invite(code) }.catch_only(InvitesRepo::Invalid) do @invites.stash_code(customer.customer_id, code).then do Payment.for(iq, @customer, @tel).then(&:write) end end end protected def finish_if_valid_invite(code) @invites.claim_code(@customer.customer_id, code) { @customer.activate_plan_starting_now }.then do Finish.new(@customer, @tel).write end end def save_customer_plan(iq, code) ParentCodeRepo.new(REDIS).find(code).then do |parent| plan = Plan.for_registration(iq.form.field("plan_name").value.to_s) @customer = @customer.with_plan(plan.name, parent_customer_id: parent) @customer.save_plan! end end class GooglePlay def initialize(customer, google_play_userid, tel) @customer = customer @google_play_userid = google_play_userid @tel = tel @invites = InvitesRepo.new(DB, REDIS) @parent_code_repo = ParentCodeRepo.new(REDIS) end def used REDIS.sismember("google_play_userids", @google_play_userid) end def form FormTemplate.render( "registration/google_play", tel: @tel ) end def write used.then do |u| next Activation.for(@customer, nil, @tel).write if u.to_s == "1" Command.reply { |reply| reply.allowed_actions = [:next] reply.command << form }.then(&method(:activate)).then do Finish.new(@customer, @tel).write end end end def activate(iq) plan = Plan.for_registration(iq.form.field("plan_name").value) code = iq.form.field("code")&.value EMPromise.all([ @parent_code_repo.find(code), REDIS.sadd("google_play_userids", @google_play_userid) ]).then { |(parent, _)| save_active_plan(plan, parent) }.then do use_referral_code(code) end end protected def save_active_plan(plan, parent) @customer = @customer.with_plan(plan.name, parent_customer_id: parent) @customer.activate_plan_starting_now end def use_referral_code(code) EMPromise.resolve(nil).then { @invites.claim_code(@customer.customer_id, code) { @customer.extend_plan } }.catch_only(InvitesRepo::Invalid) do @invites.stash_code(@customer.customer_id, code) end end end class Allow < Activation def self.for(customer, tel, jid) credit_to = CONFIG[:approved_domains][jid.domain.to_sym] new(customer, tel, credit_to) end def initialize(customer, tel, credit_to) super(customer, tel) @credit_to = credit_to end def form FormTemplate.render( "registration/allow", tel: tel, domain: customer.jid.domain ) end def next_step(iq) plan = Plan.for_registration(iq.form.field("plan_name").value.to_s) @customer = customer.with_plan(plan.name) EMPromise.resolve(nil).then { activate }.then do Finish.new(customer, tel).write end end protected def activate DB.transaction do if @credit_to InvitesRepo.new(DB, REDIS).create_claimed_code( @credit_to, customer.customer_id ) end @customer.activate_plan_starting_now end end end end module Payment def self.kinds @kinds ||= {} end def self.for(iq, customer, tel, final_message: nil, finish: Finish) kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) { raise "Invalid activation method" }.call(customer, tel, final_message: final_message, finish: finish) end class Bitcoin Payment.kinds[:bitcoin] = method(:new) THIRTY_DAYS = 60 * 60 * 24 * 30 def initialize(customer, tel, final_message: nil, **) @customer = customer @customer_id = customer.customer_id @tel = tel @final_message = final_message end attr_reader :customer_id, :tel def save REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel) end def form(rate, addr) amount = CONFIG[:activation_amount] / rate FormTemplate.render( "registration/btc", amount: amount, addr: addr, final_message: @final_message ) end def write EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)| Command.reply { |reply| reply.allowed_actions = [:prev] reply.status = :canceled reply.command << form(rate, addr) }.then(&method(:handle_possible_prev)) end end protected def handle_possible_prev(iq) raise "Action not allowed" unless iq.prev? Activation.for(@customer, nil, @tel).then(&:write) end def addr_and_rate EMPromise.all([ @customer.btc_addresses.then { |addrs| addrs.first || @customer.add_btc_address }, BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase) ]) end end class CreditCard Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } def self.for(in_customer, tel, finish: Finish, **) reload_customer(in_customer).then do |(customer, payment_methods)| if customer.balance >= CONFIG[:activation_amount_accept] next BillPlan.new(customer, tel, finish: finish) end if (method = payment_methods.default_payment_method) next Activate.new(customer, method, tel, finish: finish) end new(customer, tel, finish: finish) end end def self.reload_customer(customer) EMPromise.all([ Command.execution.customer_repo.find(customer.customer_id), customer.payment_methods ]) end def initialize(customer, tel, finish: Finish) @customer = customer @tel = tel @finish = finish end def oob(reply) oob = OOB.find_or_create(reply.command) oob.url = CONFIG[:credit_card_url].call( reply.to.stripped.to_s.gsub("\\", "%5C"), @customer.customer_id ) + "&amount=#{CONFIG[:activation_amount]}" oob.desc = "Add credit card, save, then next here to continue" oob end def write Command.reply { |reply| reply.allowed_actions = [:next, :prev] toob = oob(reply) reply.note_type = :info reply.note_text = "#{toob.desc}: #{toob.url}" }.then do |iq| next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev? CreditCard.for(@customer, @tel, finish: @finish).then(&:write) end end class Activate def initialize(customer, payment_method, tel, finish: Finish) @customer = customer @payment_method = payment_method @tel = tel @finish = finish end def write CreditCardSale.create( @customer, amount: CONFIG[:activation_amount], payment_method: @payment_method ).then( ->(_) { sold }, ->(_) { declined } ) end protected def sold BillPlan.new(@customer, @tel, finish: @finish).write end DECLINE_MESSAGE = "Your bank declined the transaction. " \ "Often this happens when a person's credit card " \ "is a US card that does not support international " \ "transactions, as JMP is not based in the USA, though " \ "we do support transactions in USD.\n\n" \ "You may add another card" def decline_oob(reply) oob = OOB.find_or_create(reply.command) oob.url = CONFIG[:credit_card_url].call( reply.to.stripped.to_s.gsub("\\", "%5C"), @customer.customer_id ) + "&amount=#{CONFIG[:activation_amount]}" oob.desc = DECLINE_MESSAGE oob end def declined Command.reply { |reply| reply_oob = decline_oob(reply) reply.allowed_actions = [:next] reply.note_type = :error reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}" }.then do CreditCard.for(@customer, @tel, finish: @finish).then(&:write) end end end end class InviteCode Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) } def self.for(in_customer, tel, finish: Finish, **) reload_customer(in_customer).then do |customer| if customer.balance >= CONFIG[:activation_amount_accept] next BillPlan.new(customer, tel, finish: finish) end msg = if customer.balance.positive? "Account balance not enough to cover the activation" end new(customer, tel, error: msg, finish: Finish) end end def self.reload_customer(customer) Command.execution.customer_repo.find(customer.customer_id) end FIELDS = [{ var: "code", type: "text-single", label: "Your referral code", required: true }].freeze def initialize(customer, tel, error: nil, finish: Finish, **) @customer = customer @tel = tel @error = error @finish = finish @parent_code_repo = ParentCodeRepo.new(REDIS) end def add_form(reply) form = reply.form form.type = :form form.title = "Enter Referral Code" form.instructions = @error if @error form.fields = FIELDS end def write Command.reply { |reply| reply.allowed_actions = [:next, :prev] add_form(reply) }.then(&method(:parse)) end def parse(iq) return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev? verify(iq.form.field("code")&.value&.to_s) .catch_only(InvitesRepo::Invalid, &method(:invalid_code)) .then(&:write) end protected def invalid_code(e) InviteCode.new(@customer, @tel, error: e.message) end def customer_id @customer.customer_id end def verify(code) @parent_code_repo.find(code).then do |parent_customer_id| if parent_customer_id set_parent(parent_customer_id) else InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) { @customer.activate_plan_starting_now }.then { Finish.new(@customer, @tel) } end end end def set_parent(parent_customer_id) @customer = @customer.with_plan( @customer.plan_name, parent_customer_id: parent_customer_id ) @customer.save_plan!.then do self.class.for(@customer, @tel, finish: @finish) end end end class Mail Payment.kinds[:mail] = method(:new) def initialize(customer, tel, final_message: nil, **) @customer = customer @tel = tel @final_message = final_message end def form FormTemplate.render( "registration/mail", currency: @customer.currency, final_message: @final_message, **onboarding_extras ) end def onboarding_extras jid = ProxiedJID.new(@customer.jid).unproxied return {} unless jid.domain == CONFIG[:onboarding_domain] { customer_id: @customer.customer_id, in_note: "Customer ID" } end def write Command.reply { |reply| reply.allowed_actions = [:prev] reply.status = :canceled reply.command << form }.then { |iq| raise "Action not allowed" unless iq.prev? Activation.for(@customer, nil, @tel).then(&:write) } end end end class BillPlan def initialize(customer, tel, finish: Finish) @customer = customer @tel = tel @finish = finish end def write @customer.bill_plan(note: "Bill #{@tel} for first month").then do @finish.new(@customer, @tel).write end end end class Finish def initialize(customer, tel) @customer = customer @tel = tel @invites = InvitesRepo.new(DB, REDIS) end def write BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid| BandwidthTNOrder.create( @tel, customer_order_id: @customer.customer_id, reservation_id: rid ).then(&:poll).then( ->(_) { customer_active_tel_purchased }, method(:number_purchase_error) ) end end protected def number_purchase_error(e) Command.log.error "number_purchase_error", e TEL_SELECTIONS.delete(@customer.jid).then { TEL_SELECTIONS[@customer.jid] }.then { |choose| choose.choose_tel( error: "The JMP number #{@tel} is no longer available." ) }.then { |tel| Finish.new(@customer, tel).write } end def raise_setup_error(e) Command.log.error "@customer.register! failed", e Command.finish( "There was an error setting up your number, " \ "please contact JMP support.", type: :error ) end def put_default_fwd Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for( uri: "xmpp:#{@customer.jid}", voicemail_enabled: true )) end def use_referral_code @invites.use_pending_group_code(@customer.customer_id).then do |credit_to| next unless credit_to Transaction.new( customer_id: @customer.customer_id, transaction_id: "referral_#{@customer.customer_id}_#{credit_to}", amount: @customer.monthly_price, note: "Referral Bonus", bonus_eligible?: false ).insert end end def customer_active_tel_purchased @customer.register!(@tel).catch(&method(:raise_setup_error)).then { EMPromise.all([ REDIS.del("pending_tel_for-#{@customer.jid}"), put_default_fwd, use_referral_code ]) }.then do FinishOnboarding.for(@customer, @tel).then(&:write) end end end module FinishOnboarding def self.for(customer, tel, db: LazyObject.new { DB }) jid = ProxiedJID.new(customer.jid).unproxied if jid.domain == CONFIG[:onboarding_domain] Snikket.for(customer, tel, db: db) else NotOnboarding.new(customer, tel) end end class Snikket def self.for(customer, tel, db:) ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is| if is.empty? new(customer, tel, db: db) elsif is[0].bootstrap_token.empty? # This is a need_dns one, try the launch again new(customer, tel, db: db).launch(is[0].domain) else GetInvite.for(customer, is[0], tel, db: db) end end end def initialize(customer, tel, error: nil, old: nil, db:) @customer = customer @tel = tel @error = error @db = db @old = old end ACTION_VAR = "http://jabber.org/protocol/commands#actions" def form FormTemplate.render( "registration/snikket", tel: @tel, error: @error ) end def write Command.reply { |reply| reply.allowed_actions = [:next] reply.command << form }.then(&method(:next_step)) end def next_step(iq) subdomain = empty_nil(iq.form.field("subdomain")&.value) domain = "#{subdomain}.snikket.chat" if iq.form.field(ACTION_VAR)&.value == "custom_domain" CustomDomain.new(@customer, @tel, old: @old).write elsif @old && (!subdomain || domain == @old.domain) GetInvite.for(@customer, @old, @tel, db: @db).then(&:write) else launch(domain) end end def launch(domain) IQ_MANAGER.write(::Snikket::Launch.new( nil, CONFIG[:snikket_hosting_api], domain: domain )).then { |launched| save_instance_and_wait(domain, launched) }.catch { |e| next EMPromise.reject(e) unless e.respond_to?(:text) Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write } end def save_instance_and_wait(domain, launched) instance = ::Snikket::CustomerInstance.for(@customer, domain, launched) repo = ::Snikket::Repo.new(db: @db) (@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old)) .then { repo.put(instance) }.then do if launched.status == :needs_dns NeedsDNS.new(@customer, instance, @tel, launched.records).write else GetInvite.for(@customer, instance, @tel, db: @db).then(&:write) end end end def empty_nil(s) s.nil? || s.empty? ? nil : s end class NeedsDNS < Snikket def initialize(customer, instance, tel, records, db: DB) @customer = customer @instance = instance @tel = tel @records = records @db = db end def form FormTemplate.render( "registration/snikket_needs_dns", records: @records ) end def write Command.reply { |reply| reply.allowed_actions = [:prev, :next] reply.command << form }.then do |iq| if iq.prev? CustomDomain.new(@customer, @tel, old: @instance).write else launch(@instance.domain) end end end end class GetInvite def self.for(customer, instance, tel, db: DB) instance.fetch_invite.then do |xmpp_uri| if xmpp_uri GoToInvite.new(xmpp_uri) else new(customer, instance, tel, db: db) end end end def initialize(customer, instance, tel, db: DB) @customer = customer @instance = instance @tel = tel @db = db end def form FormTemplate.render( "registration/snikket_wait", domain: @instance.domain ) end def write Command.reply { |reply| reply.allowed_actions = [:prev, :next] reply.command << form }.then do |iq| if iq.prev? Snikket.new(@customer, @tel, old: @instance, db: @db).write else GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write) end end end end class GoToInvite def initialize(xmpp_uri) @xmpp_uri = xmpp_uri end def write Command.finish do |reply| oob = OOB.find_or_create(reply.command) oob.url = @xmpp_uri end end end end class CustomDomain < Snikket def initialize(customer, tel, old: nil, error: nil, db: DB) @customer = customer @tel = tel @error = error @old = old @db = db end def form FormTemplate.render( "registration/snikket_custom", tel: @tel, error: @error ) end def write Command.reply { |reply| reply.allowed_actions = [:prev, :next] reply.command << form }.then do |iq| if iq.prev? Snikket.new(@customer, @tel, db: @db, old: @old).write else launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain) end end end end class NotOnboarding def initialize(customer, tel) @customer = customer @tel = tel end def write WelcomeMessage.new(@customer, @tel).welcome Command.finish("Your JMP account has been activated as #{@tel}") end end end end