M .rubocop.yml => .rubocop.yml +4 -0
@@ 10,6 10,10 @@ Metrics/MethodLength:
Exclude:
- test/*
+Metrics/AbcSize:
+ Exclude:
+ - test/*
+
Style/Tab:
Enabled: false
M lib/customer.rb => lib/customer.rb +38 -3
@@ 13,7 13,7 @@ class Customer
def self.for_customer_id(customer_id)
result = DB.query_defer(<<~SQL, [customer_id])
- SELECT COALESCE(balance,0) AS balance, plan_name
+ SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
FROM customer_plans LEFT JOIN balances USING (customer_id)
WHERE customer_id=$1 LIMIT 1
SQL
@@ 22,14 22,37 @@ class Customer
end
end
- attr_reader :balance
+ attr_reader :customer_id, :balance
- def initialize(customer_id, plan_name: nil, balance: BigDecimal.new(0))
+ def initialize(
+ customer_id,
+ plan_name: nil,
+ expires_at: Time.now,
+ balance: BigDecimal.new(0)
+ )
@plan = plan_name && Plan.for(plan_name)
+ @expires_at = expires_at
@customer_id = customer_id
@balance = balance
end
+ def with_plan(plan_name)
+ self.class.new(
+ @customer_id,
+ balance: @balance,
+ expires_at: @expires_at,
+ plan_name: plan_name
+ )
+ end
+
+ def plan_name
+ @plan.name
+ end
+
+ def currency
+ @plan.currency
+ end
+
def merchant_account
@plan.merchant_account
end
@@ 41,4 64,16 @@ class Customer
.find(@customer_id)
.then(PaymentMethods.method(:for_braintree_customer))
end
+
+ def active?
+ @plan && @expires_at > Time.now
+ end
+
+ def registered?
+ ibr = IBR.new(:get, CONFIG[:sgx])
+ ibr.from = "customer_#{@customer_id}@#{CONFIG[:component][:jid]}"
+ IQ_MANAGER.write(ibr).catch { nil }.then do |result|
+ result&.respond_to?(:registered?) && result&.registered?
+ end
+ end
end
M lib/plan.rb => lib/plan.rb +4 -0
@@ 12,6 12,10 @@ class Plan
@plan = plan
end
+ def name
+ @plan[:name]
+ end
+
def currency
@plan[:currency]
end
A lib/registration.rb => lib/registration.rb +169 -0
@@ 0,0 1,169 @@
+# frozen_string_literal: true
+
+class Registration
+ def self.for(iq, customer, web_register_manager)
+ raise "TODO" if customer&.active?
+
+ EMPromise.resolve(customer&.registered?).then do |registered|
+ if registered
+ Registered.new(iq, result.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 = :error
+ @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)
+ return EMPromise.resolve(new(iq, customer, tel)) if customer
+
+ # Create customer_id
+ raise "TODO"
+ end
+
+ def initialize(iq, customer, tel)
+ @reply = iq.reply
+ 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: "bitcoin",
+ label: "Bitcoin"
+ },
+ {
+ value: "credit_card",
+ label: "Credit Card"
+ },
+ {
+ value: "code",
+ label: "Referral or Activation Code"
+ }
+ ]
+ },
+ {
+ 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
+
+ def write
+ form = reply.form
+ form.type = :form
+ form.title = "Activate JMP"
+ form.instructions = "Going to activate #{tel} (TODO RATE CTR)"
+ form.fields = FORM_FIELDS
+
+ COMMAND_MANAGER.write(reply).then { |iq|
+ Payment.for(iq, customer, tel)
+ }.then(&:write)
+ end
+ end
+
+ module Payment
+ def self.for(iq, customer, tel)
+ case iq.form.field("activation_method")&.value&.to_s
+ when "bitcoin"
+ Bitcoin.new(iq, customer, tel)
+ when "credit_card"
+ raise "TODO"
+ when "code"
+ raise "TODO"
+ else
+ raise "Invalid activation method"
+ end
+ end
+
+ class Bitcoin
+ def initialize(iq, customer, tel)
+ @reply = iq.reply
+ reply.note_type = :info
+ reply.status = :completed
+
+ plan_name = iq.form.field("plan_name").value.to_s
+ @customer = customer.with_plan(plan_name)
+ @customer_id = customer.customer_id
+ @tel = tel
+ @addr = ELECTRUM.createnewaddress
+ end
+
+ attr_reader :reply, :customer_id, :tel
+
+ def save
+ EMPromise.all([
+ REDIS.mset(
+ "pending_tel_for-#{customer_id}", tel,
+ "pending_plan_for-#{customer_id}", @customer.plan_name
+ ),
+ @addr.then do |addr|
+ REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
+ end
+ ])
+ 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
+ end
+ end
+end
M lib/web_register_manager.rb => lib/web_register_manager.rb +1 -1
@@ 14,7 14,7 @@ class WebRegisterManager
end
def choose_tel(iq)
- self[iq.from.stripped].choose_tel(iq)
+ self[iq&.from&.stripped].choose_tel(iq)
end
class HaveTel
M sgx_jmp.rb => sgx_jmp.rb +28 -0
@@ 8,17 8,24 @@ require "dhall"
require "em-hiredis"
require "em_promise"
+require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
require_relative "lib/customer"
+require_relative "lib/electrum"
require_relative "lib/em"
+require_relative "lib/existing_registration"
require_relative "lib/payment_methods"
+require_relative "lib/registration"
require_relative "lib/transaction"
+require_relative "lib/web_register_manager"
CONFIG =
Dhall::Coder
.new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
+ELECTRUM = Electrum.new(**CONFIG[:electrum])
+
# Braintree is not async, so wrap in EM.defer for now
class AsyncBraintree
def initialize(environment:, merchant_id:, public_key:, private_key:, **)
@@ 60,13 67,16 @@ Blather::DSL.append_features(self.class)
def panic(e)
warn "Error raised during event loop: #{e.message}"
+ warn e.backtrace if e.respond_to?(:backtrace)
exit 1
end
EM.error_handler(&method(:panic))
when_ready do
+ BLATHER = self
REDIS = EM::Hiredis.connect
+ BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
DB = PG::EM::Client.new(dbname: "jmp")
DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
@@ 126,6 136,7 @@ end
IQ_MANAGER = SessionManager.new(self, :id)
COMMAND_MANAGER = SessionManager.new(self, :sessionid, timeout: 60 * 60)
+web_register_manager = WebRegisterManager.new
disco_items node: "http://jabber.org/protocol/commands" do |iq|
reply = iq.reply
@@ 136,11 147,28 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
iq.to,
"buy-credit",
"Buy account credit"
+ ),
+ Blather::Stanza::DiscoItems::Item.new(
+ iq.to,
+ "jabber:iq:register",
+ "Register"
)
]
self << reply
end
+command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
+ Customer.for_jid(iq.from.stripped).catch {
+ nil
+ }.then { |customer|
+ Registration.for(
+ iq,
+ customer,
+ web_register_manager
+ ).then(&:write)
+ }.catch(&method(:panic))
+end
+
def reply_with_note(iq, text, type: :info)
reply = iq.reply
reply.status = :completed
M test/test_helper.rb => test/test_helper.rb +11 -0
@@ 37,6 37,7 @@ CONFIG = {
component: {
jid: "component"
},
+ activation_amount: 1,
plans: [
{
name: "test_usd",
@@ 58,6 59,16 @@ BLATHER = Class.new {
def <<(*); end
}.new.freeze
+class Matching
+ def initialize(&block)
+ @block = block
+ end
+
+ def ===(other)
+ @block.call(other)
+ end
+end
+
module Minitest
class Test
def self.property(m, &block)
A test/test_registration.rb => test/test_registration.rb +154 -0
@@ 0,0 1,154 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "registration"
+
+class RegistrationTest < Minitest::Test
+ Customer::IQ_MANAGER = Minitest::Mock.new
+
+ def test_for_activated
+ skip "Registration#for activated not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ Registration.for(iq, Customer.new("test"), Minitest::Mock.new).sync
+ end
+ em :test_for_activated
+
+ def test_for_not_activated_with_customer_id
+ Customer::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Iq]
+ )
+ web_manager = WebRegisterManager.new
+ web_manager["test@example.com"] = "+15555550000"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.from = "test@example.com"
+ result = Registration.for(
+ iq,
+ Customer.new("test"),
+ web_manager
+ ).sync
+ assert_kind_of Registration::Activation, result
+ end
+ em :test_for_not_activated_with_customer_id
+
+ def test_for_not_activated_without_customer_id
+ skip "customer_id creation not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ Registration.for(iq, nil, Minitest::Mock.new).sync
+ end
+ em :test_for_not_activated_without_customer_id
+
+ class ActivationTest < Minitest::Test
+ Registration::Activation::COMMAND_MANAGER = Minitest::Mock.new
+ def setup
+ iq = Blather::Stanza::Iq::Command.new
+ @activation = Registration::Activation.new(iq, "test", "+15555550000")
+ end
+
+ def test_write
+ result = Minitest::Mock.new
+ result.expect(:then, result)
+ result.expect(:then, EMPromise.resolve(:test_result))
+ Registration::Activation::COMMAND_MANAGER.expect(
+ :write,
+ result,
+ [Blather::Stanza::Iq::Command]
+ )
+ assert_equal :test_result, @activation.write.sync
+ end
+ em :test_write
+ end
+
+ class PaymentTest < Minitest::Test
+ Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
+
+ def test_for_bitcoin
+ Registration::Payment::Bitcoin::ELECTRUM.expect(:createnewaddress, "addr")
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "bitcoin" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(
+ iq,
+ Customer.new("test"),
+ "+15555550000"
+ )
+ assert_kind_of Registration::Payment::Bitcoin, result
+ end
+
+ def test_for_credit_card
+ skip "CreditCard not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "credit_card" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(iq, "test", "+15555550000")
+ assert_kind_of Registration::Payment::CreditCard, result
+ end
+
+ def test_for_code
+ skip "Code not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "code" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(iq, "test", "+15555550000")
+ assert_kind_of Registration::Payment::Code, result
+ end
+
+ class BitcoinTest < Minitest::Test
+ Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
+ Registration::Payment::Bitcoin::REDIS = Minitest::Mock.new
+ Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new
+ Registration::Payment::Bitcoin::BLATHER = Minitest::Mock.new
+
+ def setup
+ Registration::Payment::Bitcoin::ELECTRUM.expect(
+ :createnewaddress,
+ EMPromise.resolve("testaddr")
+ )
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "plan_name", value: "test_usd" }
+ ]
+ @bitcoin = Registration::Payment::Bitcoin.new(
+ iq,
+ Customer.new("test"),
+ "+15555550000"
+ )
+ end
+
+ def test_write
+ reply_text = <<~NOTE
+ Activate your account by sending at least 1.000000 BTC to
+ testaddr
+
+ You will receive a notification when your payment is complete.
+ NOTE
+ Registration::Payment::Bitcoin::BLATHER.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal reply_text, reply.note.content
+ true
+ end]
+ )
+ Registration::Payment::Bitcoin::BTC_SELL_PRICES.expect(
+ :usd,
+ EMPromise.resolve(BigDecimal.new(1))
+ )
+ @bitcoin.stub(:save, EMPromise.resolve(nil)) do
+ @bitcoin.write.sync
+ end
+ Registration::Payment::Bitcoin::BLATHER.verify
+ end
+ em :test_write
+ end
+ end
+end