@@ -1,8 +1,151 @@
# frozen_string_literal: true
+require "lazy_object"
require "ruby-bandwidth-iris"
+require_relative "geo_code_repo"
+require_relative "local_calling_guide_repo"
class BandwidthTnRepo
+ class RateCenter
+ # @return [String, nil]
+ attr_reader :region
+
+ # @return [String, nil]
+ attr_reader :locality
+
+ # @return [String, nil]
+ attr_reader :lcg_region
+
+ # @return [String, nil]
+ attr_reader :lcg_locality
+
+ # @return [Array(String, String)]
+ def to_a
+ [region, locality]
+ end
+
+ # @param btn [BandwidthIris::Tn]
+ # @param lcg_repo [LocalCallingGuideRepo]
+ # @param geo_code_repo [GeoCodeRepo]
+ # @return [EMPromise<RateCenter>]
+ def self.for(
+ btn,
+ lcg_repo: LazyObject.new { LocalCallingGuideRepo.new },
+ geo_code_repo: LazyObject.new { GeoCodeRepo.new }
+ )
+ [
+ Source::Bandwidth.new(btn),
+ Source::Geocode.new(btn, lcg_repo, geo_code_repo),
+ Source::LCG.new(btn, lcg_repo)
+ ].reduce(EMPromise.resolve(new)) { |promise, source|
+ promise.then { |acc|
+ next acc if acc.settled?
+
+ source.fetch(acc).then(&acc.method(:fold))
+ }
+ }
+ end
+
+ # @return [Boolean]
+ def settled?
+ return false unless readable(@region)
+ return false unless readable(@locality)
+
+ true
+ end
+
+ protected
+
+ # @param value [String, nil]
+ # @return [String, nil]
+ def readable(value)
+ value if value&.match?(/\d/) == false
+ end
+
+ # @param kwargs [Hash]
+ # @return [self]
+ def fold(kwargs)
+ kwargs.each_pair do |k, v|
+ instance_variable_set(
+ :"@#{k}",
+ readable(instance_variable_get(:"@#{k}")) || v
+ )
+ end
+ self
+ end
+
+ class Source
+ # @param acc [RateCenter]
+ # @return [EMPromise<Hash>]
+ def fetch(acc)
+ raise NotImplementedError
+ end
+
+ class Bandwidth < self
+ # @param btn [BandwidthIris::Tn]
+ def initialize(btn)
+ @btn = btn
+ end
+
+ # @param _acc [RateCenter]
+ # @return [EMPromise<Hash>]
+ def fetch(_acc)
+ city, state = @btn.get_details.values_at(
+ :city, :state
+ )
+ EMPromise.resolve(
+ { locality: city, region: state }
+ )
+ rescue BandwidthIris::Errors::GenericError
+ EMPromise.resolve({})
+ end
+ end
+
+ class LCG < self
+ # @param btn [BandwidthIris::Tn]
+ # @param lcg_repo [LocalCallingGuideRepo]
+ def initialize(btn, lcg_repo)
+ @btn = btn
+ @lcg_repo = lcg_repo
+ end
+
+ # @param acc [RateCenter]
+ # @return [EMPromise<Hash>]
+ def fetch(acc)
+ EMPromise.resolve(
+ {
+ locality: acc.lcg_locality,
+ region: acc.lcg_region
+ }
+ )
+ end
+ end
+
+ class Geocode < self
+ # @param btn [BandwidthIris::Tn]
+ # @param lcg_repo [LocalCallingGuideRepo]
+ # @param geo_code_repo [GeoCodeRepo]
+ def initialize(btn, lcg_repo, geo_code_repo)
+ @btn = btn
+ @geo_code_repo = geo_code_repo
+ @lcg_repo = lcg_repo
+ end
+
+ # @param _acc [RateCenter]
+ # @return [EMPromise<Hash>]
+ def fetch(_acc)
+ npa, nxx, * = @btn.telephone_number.scan(/.{1,3}/).to_a
+ @lcg_repo.find(npa, nxx).then { |lcg|
+ cache = lcg.to_h.transform_keys(&"lcg_".method(:+))
+ @geo_code_repo.reverse(lcg.lat, lcg.lon).then { |gc|
+ gc.to_h.slice(:locality, :region).merge(cache)
+ }.catch { cache }
+ }
+ end
+ end
+ end
+ end
+
STASH_QUERY = <<~SQL
INSERT INTO tel_inventory (
tel,
@@ -21,13 +164,20 @@ class BandwidthTnRepo
)
SQL
- def initialize
+ # @param local_calling_guide_repo [LocalCallingGuideRepo]
+ # @param geo_code_repo [GeoCodeRepo]
+ def initialize(
+ local_calling_guide_repo: LazyObject.new { LocalCallingGuideRepo.new },
+ geo_code_repo: LazyObject.new { GeoCodeRepo.new }
+ )
@move_client =
BandwidthIris::Client.new(
account_id: CONFIG[:keep_area_codes_in][:account],
client_id: CONFIG[:creds][:client_id],
client_secret: CONFIG[:creds][:client_secret]
)
+ @local_calling_guide_repo = local_calling_guide_repo
+ @geo_code_repo = geo_code_repo
end
def find(tel)
@@ -48,29 +198,32 @@ class BandwidthTnRepo
raise "Could not set CNAM, please contact support"
end
- # @param [String] tel
- # @param [BandwidthIris::Tn] btn
- # @param [Numeric] premium_price
- def stash_for_later(tel, btn, premium_price)
+ # @param btn [BandwidthIris::Tn]
+ # @param premium_price [Numeric]
+ # @return [EMPromise]
+ def stash_for_later(btn, premium_price)
+ tel = "+1#{btn.telephone_number}"
LOG.info "stash_for_later #{tel}\n#{caller}"
- details = btn.get_details
- region = details[:state]
- locality = details[:city]
- params = [tel, region, locality, CONFIG[:creds][:account], premium_price]
- DB.exec(STASH_QUERY, params)
+ RateCenter.for(
+ btn,
+ lcg_repo: @local_calling_guide_repo,
+ geo_code_repo: @geo_code_repo
+ ).then { |loc|
+ DB.exec(STASH_QUERY, [tel, *loc, CONFIG[:creds][:account], premium_price])
+ }
end
def disconnect(tel, order_name)
tn = tel.sub(/\A\+1/, "")
- btn = BandwidthIris::Tn.new({ telephone_number: tn }, @move_client)
- code_and_price = CONFIG[:keep_area_codes].find { |keep|
- tn.start_with?(keep[:area_code])
+ code_and_price = CONFIG[:keep_area_codes].find { |k|
+ tn.start_with?(k[:area_code])
}
- if code_and_price
- stash_for_later(tel, btn, code_and_price[:premium_price] || 0)
- else
- BandwidthIris::Disconnect.create(order_name, tn)
+ unless code_and_price
+ return BandwidthIris::Disconnect.create(order_name, tn)
end
+
+ btn = BandwidthIris::Tn.new({ telephone_number: tn }, @move_client)
+ stash_for_later(btn, code_and_price[:premium_price] || 0)
end
def move(tel, order_name, source_account_id)
@@ -2,11 +2,22 @@
require "test_helper"
require "bandwidth_tn_repo"
+require "local_calling_guide_repo"
+require "geo_code_repo"
BandwidthTnRepo::DB = Minitest::Mock.new
class BandwidthTnRepoTest < Minitest::Test
def test_local_inventory_recycled_with_price
+ bw_body = Nokogiri::XML::Builder.new { |xml|
+ xml.Response {
+ xml.TelephoneNumberDetails {
+ xml.City "Austin"
+ xml.State "TX"
+ }
+ }
+ }.to_xml
+
stub_request(
:get,
"https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails"
@@ -18,14 +29,7 @@ class BandwidthTnRepoTest < Minitest::Test
"Authorization" => "Bearer test_bw_oauth_token",
"User-Agent" => "Ruby-Bandwidth-Iris"
}
- ).to_return(status: 200, body: <<~XML, headers: {})
- <Response>
- <TelephoneNumberDetails>
- <City>Austin</City>
- <State>TX</State>
- </TelephoneNumberDetails>
- </Response>
- XML
+ ).to_return(status: 200, body: bw_body, headers: {})
tel = "+15565555555"
@@ -38,13 +42,22 @@ class BandwidthTnRepoTest < Minitest::Test
]
)
- BandwidthTnRepo.new.disconnect(tel, "test")
+ BandwidthTnRepo.new.disconnect(tel, "test").sync
assert_mock BandwidthTnRepo::DB
end
em :test_local_inventory_recycled_with_price
def test_local_inventory_recycled_no_price
+ bw_body = Nokogiri::XML::Builder.new { |xml|
+ xml.Response {
+ xml.TelephoneNumberDetails {
+ xml.City "Austin"
+ xml.State "TX"
+ }
+ }
+ }.to_xml
+
stub_request(
:get,
"https://dashboard.bandwidth.com/v1.0/tns/5575555555/tndetails"
@@ -56,14 +69,7 @@ class BandwidthTnRepoTest < Minitest::Test
"Authorization" => "Bearer test_bw_oauth_token",
"User-Agent" => "Ruby-Bandwidth-Iris"
}
- ).to_return(status: 200, body: <<~XML, headers: {})
- <Response>
- <TelephoneNumberDetails>
- <City>Austin</City>
- <State>TX</State>
- </TelephoneNumberDetails>
- </Response>
- XML
+ ).to_return(status: 200, body: bw_body, headers: {})
tel = "+15575555555"
@@ -76,9 +82,222 @@ class BandwidthTnRepoTest < Minitest::Test
]
)
- BandwidthTnRepo.new.disconnect(tel, "test")
+ BandwidthTnRepo.new.disconnect(tel, "test").sync
assert_mock BandwidthTnRepo::DB
end
em :test_local_inventory_recycled_no_price
+
+ def test_stash_falls_back_to_lcg_on_unreadable
+ tel = "+15565555555"
+
+ # bw_r bw_l geo_r geo_l lcg_r lcg_l exp_r exp_l
+ [
+ ["TX", "Austin", "CA", "LA", "NY", "NYC", "TX", "Austin"],
+ ["TX", "1City", "CA", "LA", "NY", "NYC", "TX", "LA"],
+ ["TX", "1City", "CA", "2City", "NY", "NYC", "TX", "NYC"],
+ ["1ST", "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"],
+ ["1ST", "1City", "CA", "LA", "NY", "NYC", "CA", "LA"],
+ ["1ST", "1City", "CA", "2City", "NY", "NYC", "CA", "NYC"],
+ ["1ST", "Austin", "2ST", "LA", "NY", "NYC", "NY", "Austin"],
+ ["1ST", "1City", "2ST", "LA", "NY", "NYC", "NY", "LA"],
+ ["1ST", "1City", "2ST", "2City", "NY", "NYC", "NY", "NYC"],
+ ["TX", nil, "CA", "LA", "NY", "NYC", "TX", "LA"],
+ ["TX", nil, "CA", nil, "NY", "NYC", "TX", "NYC"],
+ [nil, "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"],
+ [nil, nil, "CA", "LA", "NY", "NYC", "CA", "LA"],
+ [nil, nil, "CA", nil, "NY", "NYC", "CA", "NYC"],
+ [nil, "Austin", nil, "LA", "NY", "NYC", "NY", "Austin"],
+ [nil, nil, nil, "LA", "NY", "NYC", "NY", "LA"],
+ [nil, nil, nil, nil, "NY", "NYC", "NY", "NYC"]
+ ].each do |bw_r, bw_l, geo_r, geo_l, lcg_r, lcg_l, exp_r, exp_l|
+ WebMock.reset!
+ stub_bw_oauth_token
+ stub_bw_tndetails(bw_r, bw_l)
+
+ lcg_body = Nokogiri::XML::Builder.new { |xml|
+ xml.root {
+ xml.prefixdata {
+ xml.send(:"rc-lat", "30.267153")
+ xml.send(:"rc-lon", "-97.743061")
+ xml.rc(lcg_l)
+ xml.region(lcg_r)
+ }
+ }
+ }.to_xml
+
+ geo_json = {
+ "latt" => "30.267153",
+ "longt" => "-97.743061"
+ }
+ geo_json["prov"] = geo_r if geo_r
+ geo_json["city"] = geo_l if geo_l
+
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails"
+ )
+ .with(
+ headers: {
+ "Accept" => "application/xml",
+ "Accept-Encoding" => "gzip, compressed",
+ "Authorization" =>
+ "Basic dGVzdF9id191c2VyOnRlc3RfYndfcGFzc3dvcmQ=",
+ "User-Agent" => "Ruby-Bandwidth-Iris"
+ }
+ ).to_return(status: 200, body: bw_body, headers: {})
+
+ stub_request(
+ :get,
+ "https://localcallingguide.com/xmlprefix.php?npa=556&nxx=555"
+ ).to_return(status: 200, body: lcg_body)
+
+ stub_request(
+ :get,
+ "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061"
+ ).to_return(status: 200, body: geo_json.to_json)
+
+ BandwidthTnRepo::DB.expect(
+ :exec,
+ nil,
+ [
+ BandwidthTnRepo::STASH_QUERY,
+ [tel, exp_r, exp_l, "test_bw_account", 10]
+ ]
+ )
+
+ BandwidthTnRepo.new(
+ local_calling_guide_repo: LocalCallingGuideRepo.new,
+ geo_code_repo: GeoCodeRepo.new(memcache: FakeMemcache.new)
+ ).disconnect(tel, "test").sync
+
+ assert_mock BandwidthTnRepo::DB
+ end
+ end
+ em :test_stash_falls_back_to_lcg_on_unreadable
+
+ def test_rate_center_resolves_region_and_locality_independently
+ fake_btn = Struct.new(:telephone_number, :bw_state, :bw_city) {
+ def get_details
+ { city: bw_city, state: bw_state }
+ end
+ }
+
+ fake_prefix = Struct.new(:lat, :lon, :region, :locality) {
+ def to_h
+ { "region" => region, "locality" => locality }
+ end
+ }
+
+ fake_geo_code = Struct.new(:locality, :region) {
+ def to_h
+ { locality: locality, region: region }
+ end
+ }
+
+ fake_lcg_repo = Struct.new(:prefix) {
+ def find(_npa, _nxx)
+ EMPromise.resolve(prefix)
+ end
+ }
+
+ fake_geo_repo = Struct.new(:result) {
+ def reverse(_lat, _lon)
+ EMPromise.resolve(result)
+ end
+ }
+
+ # bw_r bw_l geo_r geo_l lcg_r lcg_l exp_r exp_l
+ [
+ ["TX", "Austin", "CA", "LA", "NY", "NYC", "TX", "Austin"],
+ ["TX", "1City", "CA", "LA", "NY", "NYC", "TX", "LA"],
+ ["TX", "1City", "CA", "2City", "NY", "NYC", "TX", "NYC"],
+ ["1ST", "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"],
+ ["1ST", "1City", "CA", "LA", "NY", "NYC", "CA", "LA"],
+ ["1ST", "1City", "CA", "2City", "NY", "NYC", "CA", "NYC"],
+ ["1ST", "Austin", "2ST", "LA", "NY", "NYC", "NY", "Austin"],
+ ["1ST", "1City", "2ST", "LA", "NY", "NYC", "NY", "LA"],
+ ["1ST", "1City", "2ST", "2City", "NY", "NYC", "NY", "NYC"],
+ ["TX", nil, "CA", "LA", "NY", "NYC", "TX", "LA"],
+ ["TX", nil, "CA", nil, "NY", "NYC", "TX", "NYC"],
+ [nil, "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"],
+ [nil, nil, "CA", "LA", "NY", "NYC", "CA", "LA"],
+ [nil, nil, "CA", nil, "NY", "NYC", "CA", "NYC"],
+ [nil, "Austin", nil, "LA", "NY", "NYC", "NY", "Austin"],
+ [nil, nil, nil, "LA", "NY", "NYC", "NY", "LA"],
+ [nil, nil, nil, nil, "NY", "NYC", "NY", "NYC"]
+ ].each do |bw_r, bw_l, geo_r, geo_l, lcg_r, lcg_l, exp_r, exp_l|
+ btn = fake_btn.new("5565555555", bw_r, bw_l)
+ prefix = fake_prefix.new(30.0, -97.0, lcg_r, lcg_l)
+ geo = fake_geo_code.new(geo_l, geo_r)
+
+ result = BandwidthTnRepo::RateCenter.for(
+ btn,
+ lcg_repo: fake_lcg_repo.new(prefix),
+ geo_code_repo: fake_geo_repo.new(geo)
+ ).sync
+
+ assert_equal(
+ [exp_r, exp_l],
+ result.to_a,
+ "bw=#{[bw_r, bw_l]} geo=#{[geo_r, geo_l]} " \
+ "lcg=#{[lcg_r, lcg_l]}"
+ )
+ end
+ end
+ em :test_rate_center_resolves_region_and_locality_independently
+
+ def stub_bw_tndetails(bw_r, bw_l)
+ if bw_r == :error
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails"
+ ).to_return(status: 404)
+ return
+ end
+
+ bw_body = Nokogiri::XML::Builder.new { |xml|
+ xml.Response {
+ xml.TelephoneNumberDetails {
+ xml.City(bw_l) if bw_l
+ xml.State(bw_r) if bw_r
+ }
+ }
+ }.to_xml
+
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails"
+ )
+ .with(
+ headers: {
+ "Accept" => "application/xml",
+ "Accept-Encoding" => "gzip, compressed",
+ "Authorization" => "Bearer test_bw_oauth_token",
+ "User-Agent" => "Ruby-Bandwidth-Iris"
+ }
+ ).to_return(status: 200, body: bw_body, headers: {})
+ end
+
+ def stub_geocoder(geo_r, geo_l)
+ if geo_r == :error
+ stub_request(
+ :get,
+ "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061"
+ ).to_return(status: 500)
+ return
+ end
+
+ geo_json = {
+ "latt" => "30.267153",
+ "longt" => "-97.743061"
+ }
+ geo_json["prov"] = geo_r if geo_r
+ geo_json["city"] = geo_l if geo_l
+
+ stub_request(
+ :get,
+ "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061"
+ ).to_return(status: 200, body: geo_json.to_json)
+ end
end