# 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) else EMPromise.resolve(new(customer, tel)) end end def initialize(customer, tel) @customer = customer @tel = tel end attr_reader :customer, :tel FORM_FIELDS = [ { var: "activation_method", type: "list-single", label: "Activate using", required: true, options: [ { value: "credit_card", label: "Credit Card" }, { value: "bitcoin", label: "Bitcoin" }, { value: "code", label: "Invite Code" }, { value: "mail", label: "Mail or eTransfer" } ] }, { var: "plan_name", type: "list-single", label: "What currency should your account balance be in?", required: true, options: [ { value: "cad_beta_unlimited-v20210223", label: "Canadian Dollars" }, { value: "usd_beta_unlimited-v20210223", label: "United States Dollars" } ] } ].freeze ACTIVATE_INSTRUCTION = "To activate your account, you can either deposit " \ "$#{CONFIG[:activation_amount]} to your balance or enter " \ "your invite code if you have one." CRYPTOCURRENCY_INSTRUCTION = "(If you'd like to pay in a cryptocurrency other than " \ "Bitcoin, currently we recommend using a service like " \ "simpleswap.io, morphtoken.com, changenow.io, or godex.io. " \ "Manual payment via Bitcoin Cash is also available if you " \ "contact support.)" def add_instructions(form, center) center = " (#{center})" if center [ "You've selected #{tel}#{center} as your JMP number", ACTIVATE_INSTRUCTION, CRYPTOCURRENCY_INSTRUCTION ].each do |txt| form << Blather::XMPPNode.new(:instructions, form.document).tap do |i| i << txt end end end def write rate_center.then { |center| Command.reply do |reply| reply.allowed_actions = [:next] form = reply.form form.type = :form form.title = "Activate JMP" add_instructions(form, center) form.fields = FORM_FIELDS end }.then { |iq| 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 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 do |addrs| addrs.first || @customer.add_btc_address end 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 eTransfer" form.instructions = "Activate your account by sending at least " \ "$#{CONFIG[:activation_amount]}\nWe support payment by " \ "postal mail or, in Canada, by Interac eTransfer.\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 cheogram_sip_addr "sip:#{ERB::Util.url_encode(@customer.jid)}@sip.cheogram.com" 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}"), REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr), @customer.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings ]) }.then do Command.finish("Your JMP account has been activated as #{@tel}") end end end end