From 8e4c1cc7fe1a5da1fba9ba0dd650b157a5e51c17 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 10 May 2021 14:26:34 -0500 Subject: [PATCH] Happy path for credit card signup Assuming credit card payment works, you can now activate a new account by paying with one. --- config.dhall.sample | 12 ++- lib/registration.rb | 49 +++++++++-- sgx_jmp.rb | 21 +++-- test/test_helper.rb | 12 ++- test/test_registration.rb | 173 +++++++++++++++++++++++++++++++++++--- 5 files changed, 240 insertions(+), 27 deletions(-) diff --git a/config.dhall.sample b/config.dhall.sample index 843a466..ca7516f 100644 --- a/config.dhall.sample +++ b/config.dhall.sample @@ -8,11 +8,12 @@ port = 5347 }, sgx = "component2.localhost", - creds = toMap { - nick = "userid", - username = "token", - password = "secret" + creds = { + account = "00000", + username = "dashboard user", + password = "dashboard password" }, + bandwidth_site = "", braintree = { environment = "sandbox", merchant_id = "", @@ -24,6 +25,9 @@ } }, plans = ./plans.dhall + electrum = ./electrum.dhall, + oxr_app_id = "", + activation_amount = 15, credit_card_url = \(jid: Text) -> \(customer_id: Text) -> "https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}" } diff --git a/lib/registration.rb b/lib/registration.rb index b3677b6..fcb6802 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -65,7 +65,7 @@ class Registration }, { value: "credit_card", - label: "Credit Card" + label: "Credit Card ($#{CONFIG[:activation_amount]})" }, { value: "code", @@ -173,8 +173,8 @@ class Registration def self.for(iq, customer, tel) customer.payment_methods.then do |payment_methods| - if payment_methods.default_payment_method - Activate.new(iq, customer, tel) + if (method = payment_methods.default_payment_method) + Activate.new(iq, customer, method, tel) else new(iq, customer, tel) end @@ -210,10 +210,49 @@ class Registration end class Activate - def initialize(_iq, _customer, _tel) - raise "TODO" + def initialize(iq, customer, payment_method, tel) + @iq = iq + @customer = customer + @payment_method = payment_method + @tel = tel + end + + def write + Transaction.sale( + @customer.merchant_account, + @payment_method, + CONFIG[:activation_amount] + ).then(&:insert).then { + @customer.bill_plan + }.then do + Finish.new(@iq, @customer, @tel).write + end end 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.register!(@tel).then { BLATHER << @reply } }, + 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 + end end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 4402bb6..c91e432 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -8,12 +8,20 @@ require "braintree" require "dhall" require "em-hiredis" require "em_promise" +require "ruby-bandwidth-iris" + +CONFIG = + Dhall::Coder + .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) + .load(ARGV[0], transform_keys: ->(k) { k&.to_sym }) singleton_class.class_eval do include Blather::DSL Blather::DSL.append_features(self) end +require_relative "lib/backend_sgx" +require_relative "lib/bandwidth_tn_order" require_relative "lib/btc_sell_prices" require_relative "lib/buy_account_credit_form" require_relative "lib/customer" @@ -24,13 +32,15 @@ require_relative "lib/registration" require_relative "lib/transaction" require_relative "lib/web_register_manager" -CONFIG = - Dhall::Coder - .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) - .load(ARGV[0], transform_keys: ->(k) { k&.to_sym }) - ELECTRUM = Electrum.new(**CONFIG[:electrum]) +Faraday.default_adapter = :em_synchrony +BandwidthIris::Client.global_options = { + account_id: CONFIG[:creds][:account], + username: CONFIG[:creds][:username], + password: CONFIG[:creds][:password] +} + # Braintree is not async, so wrap in EM.defer for now class AsyncBraintree def initialize(environment:, merchant_id:, public_key:, private_key:, **) @@ -67,6 +77,7 @@ class AsyncBraintree end BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree]) +BACKEND_SGX = BackendSgx.new def panic(e) m = e.respond_to?(:message) ? e.message : e diff --git a/test/test_helper.rb b/test/test_helper.rb index a924ac2..b9859a3 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -32,16 +32,24 @@ rescue LoadError nil end +require "backend_sgx" + CONFIG = { sgx: "sgx", component: { jid: "component" }, + creds: { + account: "test_bw_account", + username: "test_bw_user", + password: "test_bw_password" + }, activation_amount: 1, plans: [ { name: "test_usd", - currency: :USD + currency: :USD, + monthly_price: 1000 }, { name: "test_bad_currency", @@ -56,6 +64,8 @@ CONFIG = { credit_card_url: ->(*) { "http://creditcard.example.com" } }.freeze +BACKEND_SGX = Minitest::Mock.new(BackendSgx.new) + BLATHER = Class.new { def <<(*); end }.new.freeze diff --git a/test/test_registration.rb b/test/test_registration.rb index a945e82..08c23d6 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -4,8 +4,6 @@ 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 @@ -14,10 +12,10 @@ class RegistrationTest < Minitest::Test em :test_for_activated def test_for_not_activated_with_customer_id - Customer::IQ_MANAGER.expect( - :write, + BACKEND_SGX.expect( + :registered?, EMPromise.resolve(nil), - [Blather::Stanza::Iq] + ["test"] ) web_manager = WebRegisterManager.new web_manager["test@example.com"] = "+15555550000" @@ -128,12 +126,9 @@ class RegistrationTest < Minitest::Test 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"), + Customer.new("test", plan_name: "test_usd"), "+15555550000" ) end @@ -169,15 +164,32 @@ class RegistrationTest < Minitest::Test class CreditCardTest < Minitest::Test def setup - iq = Blather::Stanza::Iq::Command.new - iq.from = "test@example.com" + @iq = Blather::Stanza::Iq::Command.new + @iq.from = "test@example.com" @credit_card = Registration::Payment::CreditCard.new( - iq, + @iq, Customer.new("test"), "+15555550000" ) end + def test_for + customer = Minitest::Mock.new(Customer.new("test")) + customer.expect( + :payment_methods, + EMPromise.resolve(OpenStruct.new(default_payment_method: :test)) + ) + assert_kind_of( + Registration::Payment::CreditCard::Activate, + Registration::Payment::CreditCard.for( + @iq, + customer, + "+15555550000" + ).sync + ) + end + em :test_for + def test_reply assert_equal [:execute, :next], @credit_card.reply.allowed_actions assert_equal( @@ -187,5 +199,142 @@ class RegistrationTest < Minitest::Test ) end end + + class ActivateTest < Minitest::Test + Registration::Payment::CreditCard::Activate::Finish = + Minitest::Mock.new + Registration::Payment::CreditCard::Activate::Transaction = + Minitest::Mock.new + + def test_write + transaction = PromiseMock.new + transaction.expect( + :insert, + EMPromise.resolve(nil) + ) + Registration::Payment::CreditCard::Activate::Transaction.expect( + :sale, + transaction, + [ + "merchant_usd", + :test_default_method, + CONFIG[:activation_amount] + ] + ) + iq = Blather::Stanza::Iq::Command.new + customer = Minitest::Mock.new( + Customer.new("test", plan_name: "test_usd") + ) + customer.expect(:bill_plan, nil) + Registration::Payment::CreditCard::Activate::Finish.expect( + :new, + OpenStruct.new(write: nil), + [Blather::Stanza::Iq, customer, "+15555550000"] + ) + Registration::Payment::CreditCard::Activate.new( + iq, + customer, + :test_default_method, + "+15555550000" + ).write.sync + Registration::Payment::CreditCard::Activate::Transaction.verify + transaction.verify + customer.verify + end + em :test_write + end + end + + class FinishTest < Minitest::Test + Registration::Finish::BLATHER = Minitest::Mock.new + + def setup + @finish = Registration::Finish.new( + Blather::Stanza::Iq::Command.new, + Customer.new("test"), + "+15555550000" + ) + end + + def test_write + create_order = stub_request( + :post, + "https://dashboard.bandwidth.com/v1.0/accounts//orders" + ).to_return(status: 201, body: <<~RESPONSE) + + + test_order + + + RESPONSE + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order" + ).to_return(status: 201, body: <<~RESPONSE) + + COMPLETE + + RESPONSE + BACKEND_SGX.expect( + :register!, + EMPromise.resolve(OpenStruct.new(error?: false)), + ["test", "+15555550000"] + ) + Registration::Finish::BLATHER.expect( + :<<, + nil, + [Matching.new do |reply| + assert_equal :completed, reply.status + assert_equal :info, reply.note_type + assert_equal( + "Your JMP account has been activated as +15555550000", + reply.note.content + ) + end] + ) + @finish.write.sync + assert_requested create_order + BACKEND_SGX.verify + Registration::Finish::BLATHER.verify + end + em :test_write + + def test_write_tn_fail + create_order = stub_request( + :post, + "https://dashboard.bandwidth.com/v1.0/accounts//orders" + ).to_return(status: 201, body: <<~RESPONSE) + + + test_order + + + RESPONSE + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order" + ).to_return(status: 201, body: <<~RESPONSE) + + FAILED + + RESPONSE + Registration::Finish::BLATHER.expect( + :<<, + nil, + [Matching.new do |reply| + assert_equal :completed, reply.status + assert_equal :error, reply.note_type + assert_equal( + "The JMP number +15555550000 is no longer available, " \ + "please visit https://jmp.chat and choose another.", + reply.note.content + ) + end] + ) + @finish.write.sync + assert_requested create_order + Registration::Finish::BLATHER.verify + end + em :test_write_tn_fail end end -- 2.38.5