# frozen_string_literal: true require "erb" require "ruby-bandwidth-iris" require "securerandom" require_relative "./alt_top_up_form" require_relative "./command" require_relative "./bandwidth_tn_order" require_relative "./em" require_relative "./oob" require_relative "./tel_selections" class Registration def self.for(customer, tel_selections) if (reg = customer.registered?) Registered.new(reg.phone) else tel_selections[customer.jid].then(&:choose_tel).then do |tel| Activation.for(customer, tel) end end end class Registered def initialize(tel) @tel = tel end def write Command.finish("You are already registered with JMP number #{@tel}") end end class Activation def self.for(customer, tel) if customer.active? Finish.new(customer, tel) elsif CONFIG[:approved_domains].key?(customer.jid.domain.to_sym) credit_to = CONFIG[:approved_domains][customer.jid.domain.to_sym] Allow.new(customer, tel, credit_to) else new(customer, tel) end end def initialize(customer, tel) @customer = customer @tel = tel end attr_reader :customer, :tel def form(center) FormTemplate.render( "registration/activate", tel: tel, rate_center: center ) end def write rate_center.then { |center| Command.reply do |reply| reply.allowed_actions = [:next] reply.command << form(center) end }.then(&method(:next_step)) end def next_step(iq) EMPromise.resolve(nil).then { Payment.for(iq, customer, tel) }.then(&:write) end protected def rate_center EM.promise_fiber { center = BandwidthIris::Tn.get(tel).get_rate_center "#{center[:rate_center]}, #{center[:state]}" }.catch { nil } end class Allow < Activation def initialize(customer, tel, credit_to) super(customer, tel) @credit_to = credit_to end def form(center) FormTemplate.render( "registration/allow", tel: tel, rate_center: center, domain: customer.jid.domain ) end def next_step(iq) plan_name = 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 DB.exec(<<~SQL, [@credit_to, customer.customer_id]) INSERT INTO invites (creator_id, used_by_id, used_at) VALUES ($1, $2, LOCALTIMESTAMP) SQL 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) plan_name = iq.form.field("plan_name").value.to_s customer = customer.with_plan(plan_name) 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 EMPromise.all([ REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel), REDIS.setex( "pending_plan_for-#{customer_id}", THIRTY_DAYS, @customer.plan_name ) ]) end def note_text(amount, addr) <<~NOTE Activate your account by sending at least #{'%.6f' % amount} BTC to #{addr} You will receive a notification when your payment is complete. NOTE end def write EMPromise.all([ addr, save, BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase) ]).then do |(addr, _, rate)| min = CONFIG[:activation_amount] / rate Command.finish( note_text(min, addr) + @final_message.to_s, status: :canceled ) end end protected def addr @addr ||= @customer.btc_addresses.then { |addrs| addrs.first || @customer.add_btc_address } end end class CreditCard Payment.kinds[:credit_card] = ->(*args) { self.for(*args) } def self.for(customer, tel, finish: Finish, **) customer.payment_methods.then do |payment_methods| if (method = payment_methods.default_payment_method) Activate.new(customer, method, tel, finish: finish) else new(customer, tel, finish: finish) end end 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 ) oob.desc = "Add credit card, then return here to continue" oob end def write Command.reply { |reply| reply.allowed_actions = [:next] reply.note_type = :info reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}" }.then do 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 Transaction.sale( @customer, amount: CONFIG[:activation_amount], payment_method: @payment_method ).then( method(:sold), ->(_) { declined } ) end protected def sold(tx) tx.insert.then { @customer.bill_plan }.then do @finish.new(@customer, @tel).write end 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" \ "If you were trying a prepaid card, you may wish to use "\ "Privacy.com instead, as they do support international " \ "transactions.\n\n " \ "You may add another card and then return here" 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 ) 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] = method(:new) class Invalid < StandardError; end FIELDS = [{ var: "code", type: "text-single", label: "Your invite code", required: true }].freeze def initialize(customer, tel, error: nil, **) @customer = customer @tel = tel @error = error end def add_form(reply) form = reply.form form.type = :form form.title = "Enter Invite Code" form.instructions = @error if @error form.fields = FIELDS end def write Command.reply { |reply| reply.allowed_actions = [:next] add_form(reply) }.then(&method(:parse)) end def parse(iq) guard_too_many_tries.then { verify(iq.form.field("code")&.value&.to_s) }.then { Finish.new(@customer, @tel) }.catch_only(Invalid, &method(:invalid_code)).then(&:write) end protected def guard_too_many_tries REDIS.get("jmp_invite_tries-#{customer_id}").then do |t| raise Invalid, "Too many wrong attempts" if t.to_i > 10 end end def invalid_code(e) EMPromise.all([ REDIS.incr("jmp_invite_tries-#{customer_id}").then do REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60) end, InviteCode.new(@customer, @tel, error: e.message) ]).then(&:last) end def customer_id @customer.customer_id end def verify(code) EMPromise.resolve(nil).then do DB.transaction do valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive? UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP WHERE code=$2 AND used_by_id IS NULL SQL raise Invalid, "Not a valid invite code: #{code}" unless valid @customer.activate_plan_starting_now end end end end class Mail Payment.kinds[:mail] = method(:new) def initialize(_customer, _tel, final_message: nil, **) @final_message = final_message end def form form = Blather::Stanza::X.new(:result) form.title = "Activate by Mail or Interac e-Transfer" form.instructions = "Activate your account by sending at least " \ "$#{CONFIG[:activation_amount]}\nWe support payment by " \ "postal mail or, in Canada, by Interac e-Transfer.\n\n" \ "You will receive a notification when your payment is complete." \ "#{@final_message}" form.fields = fields.to_a form end def fields [ AltTopUpForm::MAILING_ADDRESS, AltTopUpForm::IS_CAD ].flatten end def write Command.finish(status: :canceled) do |reply| reply.command << form end end end end class Finish def initialize(customer, tel) @customer = customer @tel = tel end def write BandwidthTNOrder.create(@tel).then(&:poll).then( ->(_) { customer_active_tel_purchased }, ->(_) { number_purchase_error } ) end protected def number_purchase_error TEL_SELECTIONS.delete(@customer.jid).then { TelSelections::ChooseTel.new.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 customer_active_tel_purchased @customer.register!(@tel).catch(&method(:raise_setup_error)).then { EMPromise.all([ REDIS.del("pending_tel_for-#{@customer.jid}"), Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for( uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings )) ]) }.then do Command.finish("Your JMP account has been activated as #{@tel}") end end end end