From b28d7c5680144d349f63cd8b30c6a04d77486856 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 2 May 2023 13:54:59 -0500 Subject: [PATCH] When no results for a city, try area codes nearby --- lib/registration.rb | 4 +- lib/tel_selections.rb | 103 ++++++++++++++++++++++++++++++------ sgx_jmp.rb | 3 +- test/test_helper.rb | 20 ++++++- test/test_registration.rb | 16 ++++-- test/test_tel_selections.rb | 65 +++++++++++++++++++++-- 6 files changed, 183 insertions(+), 28 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index 27759bfa45872314beed3f9e24aac2686ed824e9..8d6338d078372616a822abae32d9740623e20133 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -542,7 +542,9 @@ class Registration def number_purchase_error(e) Command.log.error "number_purchase_error", e TEL_SELECTIONS.delete(@customer.jid).then { - TelSelections::ChooseTel.new.choose_tel( + TEL_SELECTIONS[@customer.jid] + }.then { |choose| + choose.choose_tel( error: "The JMP number #{@tel} is no longer available." ) }.then { |tel| Finish.new(@customer, tel).write } diff --git a/lib/tel_selections.rb b/lib/tel_selections.rb index 9b7cbe3e8f5a9cca2d88fc78606f76b4c5aea5ac..da35f883e0583191adedc88c670f04f0544e2829 100644 --- a/lib/tel_selections.rb +++ b/lib/tel_selections.rb @@ -3,13 +3,18 @@ require "ruby-bandwidth-iris" Faraday.default_adapter = :em_synchrony +require "cbor" + +require_relative "area_code_repo" require_relative "form_template" class TelSelections THIRTY_DAYS = 60 * 60 * 24 * 30 - def initialize(redis: REDIS) + def initialize(redis: REDIS, db: DB, memcache: MEMCACHE) @redis = redis + @memcache = memcache + @db = db end def set(jid, tel) @@ -22,7 +27,7 @@ class TelSelections def [](jid) @redis.get("pending_tel_for-#{jid}").then do |tel| - tel ? HaveTel.new(tel) : ChooseTel.new + tel ? HaveTel.new(tel) : ChooseTel.new(db: @db, memcache: @memcache) end end @@ -37,12 +42,17 @@ class TelSelections end class ChooseTel + def initialize(db: DB, memcache: MEMCACHE) + @db = db + @memcache = memcache + end + def choose_tel(error: nil) Command.reply { |reply| reply.allowed_actions = [:next] reply.command << FormTemplate.render("tn_search", error: error) }.then do |iq| - available = AvailableNumber.for(iq.form) + available = AvailableNumber.for(iq.form, db: @db, memcache: @memcache) next available if available.is_a?(String) choose_from_list(available.tns) @@ -66,27 +76,57 @@ class TelSelections end class AvailableNumber - def self.for(form) - q = form.field("q")&.value.to_s.strip - return q if q =~ /\A\+1\d{10}\Z/ + def self.for(form, db: DB, memcache: MEMCACHE) + qs = form.field("q")&.value.to_s.strip + return qs if qs =~ /\A\+1\d{10}\Z/ + + q = Q.for(qs, db: db, memcache: memcache) new( - Q - .for(q).iris_query + q.iris_query .merge(enableTNDetail: true, LCA: false) - .merge(Quantity.for(form).iris_query) + .merge(Quantity.for(form).iris_query), + fallback: q.fallback, + memcache: memcache ) end - def initialize(iris_query) + def initialize(iris_query, fallback: [], memcache: MEMCACHE) @iris_query = iris_query + @fallback = fallback + @memcache = memcache end def tns Command.log.debug("BandwidthIris::AvailableNumber.list", @iris_query) - BandwidthIris::AvailableNumber.list(@iris_query).map do |tn| - Tn.new(**tn) + unless (result = fetch_cache) + result = BandwidthIris::AvailableNumber.list(@iris_query) end + return next_fallback if result.empty? && !@fallback.empty? + + result.map { |tn| Tn.new(**tn) } + end + + def next_fallback + @memcache.set(cache_key, CBOR.encode([]), 43200) + self.class.new( + @fallback.shift.iris_query.merge( + enableTNDetail: true, quantity: @iris_query[:quantity] + ), + fallback: @fallback, + memcache: @memcache + ).tns + end + + def fetch_cache + promise = EMPromise.new + @memcache.get(cache_key, &promise.method(:fulfill)) + result = promise.sync + result ? CBOR.decode(result) : nil + end + + def cache_key + "BandwidthIris_#{@iris_query.to_a.flatten.join(',')}" end class Quantity @@ -163,10 +203,10 @@ class TelSelections @queries << [regex, block] end - def self.for(q) + def self.for(q, **kwa) @queries.each do |(regex, block)| match_data = (q =~ regex) - return block.call($1 || $&, *$~.to_a[2..-1]) if match_data + return block.call($1 || $&, *$~.to_a[2..-1], **kwa) if match_data end raise "Format not recognized: #{q}" @@ -176,6 +216,10 @@ class TelSelections @q = q end + def fallback + [] + end + { areaCode: [:AreaCode, /\A[2-9][0-9]{2}\Z/], npaNxx: [:NpaNxx, /\A(?:[2-9][0-9]{2}){2}\Z/], @@ -193,7 +237,7 @@ class TelSelections ) args[1..-1].each do |regex| - register(regex) { |q| klass.new(q) } + register(regex) { |q, **| klass.new(q) } end end @@ -204,13 +248,21 @@ class TelSelections "QC" => "PQ" }.freeze - def initialize(state) + def initialize(state, **) @state = STATE_MAP.fetch(state.upcase, state.upcase) end + def fallback + [] + end + def iris_query { state: @state } end + + def to_s + @state + end end class CityState @@ -229,14 +281,31 @@ class TelSelections "west durham" => "Durham" }.freeze - def initialize(city, state) + def initialize(city, state, db: DB, memcache: MEMCACHE) @city = CITY_MAP.fetch(city.downcase, city) @state = State.new(state) + @db = db + @memcache = memcache + end + + def fallback + LazyObject.new do + AreaCodeRepo.new( + db: @db, + geo_code_repo: GeoCodeRepo.new(memcache: @memcache) + ).find(to_s).sync.map { |area_code| + AreaCode.new(area_code) + } + end end def iris_query @state.iris_query.merge(city: @city) end + + def to_s + "#{@city}, #{@state}" + end end end end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index d1283392a63696508477a33b542ac7d3ab1c2e0c..3e8b309f9f0149cfdee19504394759d247e0a299 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -238,9 +238,10 @@ when_ready do log.info "Ready" BLATHER = self REDIS = EM::Hiredis.connect - TEL_SELECTIONS = TelSelections.new + MEMCACHE = EM::P::Memcache.connect BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id]) DB = Postgres.connect(dbname: "jmp") + TEL_SELECTIONS = TelSelections.new DB.hold do |conn| conn.query("LISTEN low_balance") diff --git a/test/test_helper.rb b/test/test_helper.rb index 8aec8f80932d69d83a474e838522e7961d414097..9dbe116dc6bb8a1b14ca903def36428a37cf8b19 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -138,6 +138,8 @@ LOG = Class.new { Minitest::Mock.new end + def debug(*); end + def info(*); end def error(*); end @@ -210,7 +212,7 @@ class FakeTelSelections def [](jid) @selections.fetch(jid) do - TelSelections::ChooseTel.new + TelSelections::ChooseTel.new(db: FakeDB.new, memcache: FakeMemcache.new) end end end @@ -338,6 +340,22 @@ class FakeDB end end +class FakeMemcache + def initialize(data={}) + @data = data + end + + def set(k, v, _expires=nil) + raise "No spaces" if k =~ /\s/ + + @data[k] = v + end + + def get(k) + yield @data[k] + end +end + class FakeLog def initialize @logs = [] diff --git a/test/test_registration.rb b/test/test_registration.rb index 1b194c0c8a4e47e31b8eb4227ed5fd2efbbf170e..7be8c1de9bb6579713097477d7425d14a07959c8 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -29,7 +29,9 @@ class RegistrationTest < Minitest::Test :post, "https://dashboard.bandwidth.com/v1.0/accounts//tnreservation" ) - web_manager = TelSelections.new(redis: FakeRedis.new) + web_manager = TelSelections.new( + redis: FakeRedis.new, db: FakeDB.new, memcache: FakeMemcache.new + ) web_manager.set("test@example.net", "+15555550000") result = execute_command do sgx = OpenStruct.new(registered?: false) @@ -50,7 +52,9 @@ class RegistrationTest < Minitest::Test def test_for_not_activated_approved sgx = OpenStruct.new(registered?: false) - web_manager = TelSelections.new(redis: FakeRedis.new) + web_manager = TelSelections.new( + redis: FakeRedis.new, db: FakeDB.new, memcache: FakeMemcache.new + ) web_manager.set("test\\40approved.example.com@component", "+15555550000") iq = Blather::Stanza::Iq::Command.new iq.from = "test@approved.example.com" @@ -70,7 +74,9 @@ class RegistrationTest < Minitest::Test def test_for_not_activated_googleplay sgx = OpenStruct.new(registered?: false) - web_manager = TelSelections.new(redis: FakeRedis.new) + web_manager = TelSelections.new( + redis: FakeRedis.new, db: FakeDB.new, memcache: FakeMemcache.new + ) web_manager.set("test@example.net", "+15555550000") iq = Blather::Stanza::Iq::Command.new iq.from = "test@approved.example.com" @@ -87,7 +93,9 @@ class RegistrationTest < Minitest::Test def test_for_not_activated_with_customer_id sgx = OpenStruct.new(registered?: false) - web_manager = TelSelections.new(redis: FakeRedis.new) + web_manager = TelSelections.new( + redis: FakeRedis.new, db: FakeDB.new, memcache: FakeMemcache.new + ) web_manager.set("test@example.net", "+15555550000") iq = Blather::Stanza::Iq::Command.new iq.from = "test@example.com" diff --git a/test/test_tel_selections.rb b/test/test_tel_selections.rb index 5bc15b7ae4fc4f18c64482c0f94fc51043008170..100aad6459cf200e2013f4ca45abbf9789be0801 100644 --- a/test/test_tel_selections.rb +++ b/test/test_tel_selections.rb @@ -5,7 +5,11 @@ require "tel_selections" class TelSelectionsTest < Minitest::Test def setup - @manager = TelSelections.new(redis: FakeRedis.new) + @manager = TelSelections.new( + redis: FakeRedis.new, + db: FakeDB.new, + memcache: FakeMemcache.new + ) end def test_set_get @@ -27,7 +31,7 @@ class TelSelectionsTest < Minitest::Test form = Blather::Stanza::X.new form.fields = [{ var: "q", value: "226" }] iris_query = TelSelections::ChooseTel::AvailableNumber - .for(form) + .for(form, db: FakeDB.new, memcache: FakeMemcache.new) .instance_variable_get(:@iris_query) assert_equal( { areaCode: "226", enableTNDetail: true, LCA: false, quantity: 10 }, @@ -44,13 +48,62 @@ class TelSelectionsTest < Minitest::Test end end iris_query = TelSelections::ChooseTel::AvailableNumber - .for(form) + .for(form, db: FakeDB.new, memcache: FakeMemcache.new) .instance_variable_get(:@iris_query) assert_equal( { areaCode: "226", enableTNDetail: true, LCA: false, quantity: 500 }, iris_query ) end + + def test_fallback + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/accounts//availableNumbers" \ + "?city=Kitchener-Waterloo&enableTNDetail=true&lCA=false&" \ + "quantity=10&state=ON" + ).to_return(status: 200, body: "") + + stub_request( + :get, + "https://geocoder.ca/?json=1&locate=Kitchener-Waterloo,%20ON" + ).to_return(status: 200, body: { + postal: "N2H", longt: 0, latt: 0 + }.to_json) + + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/accounts//availableNumbers" \ + "?areaCode=226&enableTNDetail=true&quantity=10" + ).to_return(status: 200, body: <<~XML) + + + + 22655512345 + Somewhere + ON + + + + XML + + db = FakeDB.new( + ["CA", "POINT(0.0000000000 0.0000000000)", 3] => + [{ "area_code" => "226" }] + ) + form = Blather::Stanza::X.new + form.fields = [{ var: "q", value: "Kitchener, ON" }] + tns = execute_command do + TelSelections::ChooseTel::AvailableNumber + .for(form, db: db, memcache: FakeMemcache.new) + .tns + end + assert_equal( + ["(226) 555-12345 (Somewhere, ON)"], + tns.map(&:to_s) + ) + end + em :test_fallback end class TnTest < Minitest::Test @@ -123,7 +176,11 @@ class TelSelectionsTest < Minitest::Test end def test_for_citystate - q = TelSelections::ChooseTel::Q.for("Toronto, ON") + q = TelSelections::ChooseTel::Q.for( + "Toronto, ON", + db: FakeDB.new, + memcache: FakeMemcache.new + ) assert_equal({ city: "Toronto", state: "ON" }, q.iris_query) end