# 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 "./command"
require_relative "./em"
require_relative "./invites_repo"
require_relative "./oob"
require_relative "./proxied_jid"
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|
FinishOrStartActivation.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 FinishOrStartActivation
def self.for(customer, tel)
if customer.active?
Finish.new(customer, tel)
elsif customer.balance >= CONFIG[:activation_amount_accept]
BillPlan.new(customer, tel)
else
new(customer, tel)
end
end
def initialize(customer, tel)
@customer = customer
@tel = tel
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, @tel).write }
end
end
class Activation
def self.for(customer, tel)
jid = ProxiedJID.new(customer.jid).unproxied
if CONFIG[:approved_domains].key?(jid.domain.to_sym)
Allow.for(customer, tel, jid)
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 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(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)
customer.save_plan!.then do
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
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 note_text(rate, addr)
amount = CONFIG[:activation_amount] / rate
<<~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_and_rate, save]).then do |((addr, rate), _)|
Command.reply { |reply|
reply.allowed_actions = [:prev]
reply.status = :canceled
reply.note_type = :info
reply.note_text = note_text(rate, addr) + @final_message.to_s
}.then(&method(:handle_possible_prev))
end
end
protected
def handle_possible_prev(iq)
raise "Action not allowed" unless iq.prev?
Activation.for(@customer, @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) { 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 do
BillPlan.new(@customer, @tel, finish: @finish).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)
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(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
end
protected
def guard_too_many_tries
REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
raise InvitesRepo::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)
InvitesRepo.new(DB).claim_code(customer_id, code) do
@customer.activate_plan_starting_now
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 BillPlan
def initialize(customer, tel, finish: Finish)
@customer = customer
@tel = tel
@finish = finish
end
def write
@customer.bill_plan(note: "Bill for first month").then do
@finish.new(@customer, @tel).write
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