Detailed changes
@@ -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 }
@@ -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
@@ -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")
@@ -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 = []
@@ -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"
@@ -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)
+ <SearchResult>
+ <TelephoneNumberList>
+ <TelephoneNumber>
+ <FullNumber>22655512345</FullNumber>
+ <City>Somewhere</City>
+ <State>ON</State>
+ </TelephoneNumber>
+ </TelephoneNumberList>
+ </SearchResult>
+ 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