# 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 "./em"
require_relative "./error_to_send"
require_relative "./oob"
require_relative "./web_register_manager"
class Registration
def self.for(iq, customer, web_register_manager)
customer.registered?.then do |registered|
if registered
Registered.new(iq, registered.phone)
else
web_register_manager.choose_tel(iq).then do |(riq, tel)|
Activation.for(riq, customer, tel)
end
end
end
end
class Registered
def initialize(iq, tel)
@reply = iq.reply
@reply.status = :completed
@tel = tel
end
def write
@reply.note_type = :info
@reply.note_text = <<~NOTE
You are already registered with JMP number #{@tel}
NOTE
BLATHER << @reply
nil
end
end
class Activation
def self.for(iq, customer, tel)
if customer.active?
Finish.new(iq, customer, tel)
else
EMPromise.resolve(new(iq, customer, tel))
end
end
def initialize(iq, customer, tel)
@reply = iq.reply
@reply.status = :executing
@reply.allowed_actions = [:next]
@customer = customer
@tel = tel
end
attr_reader :reply, :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 do |center|
form = reply.form
form.type = :form
form.title = "Activate JMP"
add_instructions(form, center)
form.fields = FORM_FIELDS
COMMAND_MANAGER.write(reply).then { |iq|
Payment.for(iq, customer, tel)
}.then(&:write)
end
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)
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(iq, customer, tel)
end
class Bitcoin
Payment.kinds[:bitcoin] = method(:new)
def initialize(iq, customer, tel)
@reply = iq.reply
reply.note_type = :info
reply.status = :canceled
@customer = customer
@customer_id = customer.customer_id
@tel = tel
end
attr_reader :reply, :customer_id, :tel
def legacy_session_save
sid = SecureRandom.hex
REDIS.mset(
"reg-sid_for-#{customer_id}", sid,
"reg-session_tel-#{sid}", tel
)
end
def save
EMPromise.all([
legacy_session_save,
REDIS.mset(
"pending_tel_for-#{customer_id}", tel,
"pending_plan_for-#{customer_id}", @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
reply.note_text = note_text(min, addr)
BLATHER << reply
nil
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(iq, customer, tel)
customer.payment_methods.then do |payment_methods|
if (method = payment_methods.default_payment_method)
Activate.new(iq, customer, method, tel)
else
new(iq, customer, tel)
end
end
end
def initialize(iq, customer, tel)
@customer = customer
@tel = tel
@reply = iq.reply
@reply.status = :executing
@reply.allowed_actions = [:next]
@reply.note_type = :info
@reply.note_text = "#{oob.desc}: #{oob.url}"
end
attr_reader :reply
def oob
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_MANAGER.write(@reply).then do |riq|
CreditCard.for(riq, @customer, @tel).write
end
end
class Activate
def initialize(iq, customer, payment_method, tel)
@iq = iq
@customer = customer
@payment_method = payment_method
@tel = tel
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(@iq, @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
reply = @iq.reply
reply_oob = decline_oob(reply)
reply.status = :executing
reply.allowed_actions = [:next]
reply.note_type = :error
reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
COMMAND_MANAGER.write(reply).then do |riq|
CreditCard.for(riq, @customer, @tel).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(iq, customer, tel, error: nil)
@customer = customer
@tel = tel
@reply = iq.reply
@reply.status = :executing
@reply.allowed_actions = [:next]
@form = @reply.form
@form.type = :form
@form.title = "Enter Invite Code"
@form.instructions = error
@form.fields = FIELDS
end
def write
COMMAND_MANAGER.write(@reply).then do |iq|
guard_too_many_tries.then {
verify(iq.form.field("code")&.value&.to_s)
}.then {
Finish.new(iq, @customer, @tel)
}.catch_only(Invalid) { |e|
invalid_code(iq, e)
}.then(&:write)
end
end
protected
def guard_too_many_tries
REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
raise Invalid, "Too many wrong attempts" if t.to_i > 10
end
end
def invalid_code(iq, e)
EMPromise.all([
REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
end,
InviteCode.new(iq, @customer, @tel, error: e.message)
]).then(&:last)
end
def customer_id
@customer.customer_id
end
def verify(code)
EM.promise_fiber 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(iq, _customer, _tel)
@reply = iq.reply
@reply.status = :canceled
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."
form.fields = fields.to_a
form
end
def fields
[
AltTopUpForm::MAILING_ADDRESS,
AltTopUpForm::IS_CAD
].flatten
end
def write
@reply.command << form
BLATHER << @reply
end
end
end
class Finish
def initialize(iq, customer, tel)
@reply = iq.reply
@reply.status = :completed
@reply.note_type = :info
@reply.note_text = "Your JMP account has been activated as #{tel}"
@customer = customer
@tel = tel
end
def write
BandwidthTNOrder.create(@tel).then(&:poll).then(
->(_) { customer_active_tel_purchased },
lambda do |_|
@reply.note_type = :error
@reply.note_text =
"The JMP number #{@tel} is no longer available, " \
"please visit https://jmp.chat and choose another."
BLATHER << @reply
end
)
end
protected
def cheogram_sip_addr
"sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
end
def raise_setup_error
@reply.note_type = :error
@reply.note_text =
"There was an error setting up your number, " \
"please contact JMP support."
raise ErrorToSend, @reply
end
def customer_active_tel_purchased
@customer.register!(@tel).catch { |e|
puts e
raise_setup_error
}.then {
EMPromise.all([
REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
@customer.fwd_timeout = 25 # ~5 seconds / ring, 5 rings
])
}.then { BLATHER << @reply }
end
end
end