diff --git a/.rubocop.yml b/.rubocop.yml
index 34eb5657f4c2660aff1f6ec90ee68e9d37796da9..a736a631ae148fb69be0ecccf5e215fef82a6841 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -16,6 +16,7 @@ Metrics/BlockLength:
ExcludedMethods:
- route
- "on"
+ - json
Exclude:
- test/*
diff --git a/config-schema.dhall b/config-schema.dhall
index fff429e7654e1b5e26ac4cd6e1639a94d5e6ca9a..7b3c24875ba72a9b73a98a35ad848a00892ecb31 100644
--- a/config-schema.dhall
+++ b/config-schema.dhall
@@ -12,6 +12,8 @@
, private_key : Text
, public_key : Text
}
+, bulk_order_tokens :
+ List { mapKey : Text, mapValue : { customer_id : Text, peer_id : Text } }
, churnbuster : { account_id : Text, api_key : Text }
, component : { jid : Text, secret : Text }
, credit_card_url : forall (jid : Text) -> forall (customer_id : Text) -> Text
diff --git a/config.dhall.sample b/config.dhall.sample
index ba026d00b6478a2ac40db46acc6a734d18ace475..a2c8842c09bb3fb8587b557855daea2277d0eb9c 100644
--- a/config.dhall.sample
+++ b/config.dhall.sample
@@ -118,5 +118,6 @@ in
churnbuster = {
api_key = "",
account_id = ""
- }
+ },
+ bulk_order_tokens = toMap { sometoken = { customer_id = "somecustomer", peer_id = "" } }
}
diff --git a/lib/bandwidth_tn_order.rb b/lib/bandwidth_tn_order.rb
index b07a44e8a806a4e38a9c667aa2d9dfa7f12da8f6..2ae770b344b0c701a974e64e5582288308d8e960 100644
--- a/lib/bandwidth_tn_order.rb
+++ b/lib/bandwidth_tn_order.rb
@@ -26,12 +26,18 @@ class BandwidthTNOrder
reservation_id: nil,
**kwargs
)
+ create_custom(
+ name: name, **kwargs,
+ existing_telephone_number_order_type: {
+ telephone_number_list: { telephone_number: [tel.sub(/^\+?1?/, "")] }
+ }.merge(ReservationIdList.new(reservation_id).to_h)
+ )
+ end
+
+ def self.create_custom(name:, **kwargs)
EMPromise.resolve(nil).then do
Received.new(BandwidthIris::Order.create(
- name: name, **ORDER_DEFAULTS, **kwargs,
- existing_telephone_number_order_type: {
- telephone_number_list: { telephone_number: [tel.sub(/^\+?1?/, "")] }
- }.merge(ReservationIdList.new(reservation_id).to_h)
+ name: name, **ORDER_DEFAULTS, **kwargs
))
end
end
@@ -79,7 +85,14 @@ class BandwidthTNOrder
end
def tel
- "+1#{@order.completed_numbers[:telephone_number][:full_number]}"
+ tels[0]
+ end
+
+ def tels
+ tns = @order.completed_numbers[:telephone_number]
+ (tns.is_a?(Array) ? tns : [tns]).map do |item|
+ "+1#{item[:full_number]}"
+ end
end
def poll
diff --git a/lib/transaction.rb b/lib/transaction.rb
index c880df6206a75fca5b910d269893dec7c72bfdc9..3ec445d30dfdf212aa61e4fb6431b424d2a589d1 100644
--- a/lib/transaction.rb
+++ b/lib/transaction.rb
@@ -15,6 +15,7 @@ class Transaction
amount BigDecimal, coerce: ->(x) { BigDecimal(x, 4) }
note String
bonus_eligible? Bool(), default: true
+ ignore_duplicate Bool(), default: false
end
def insert
@@ -72,6 +73,7 @@ class Transaction
(customer_id, transaction_id, created_at, settled_after, amount, note)
VALUES
($1, $2, $3, $4, $5, $6)
+ #{ignore_duplicate ? 'ON CONFLICT (transaction_id) DO NOTHING' : ''}
SQL
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 6235ecfed2e7dae17d3dafeb2bb5f9a861fb797f..539f3e5ae84b5135bdc65c7f1cf3d9f9677a77ec 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -163,7 +163,11 @@ CONFIG = {
onboarding_domain: "onboarding.example.com",
adr: "A Mailing Address",
interac: "interac@example.com",
- support_link: ->(*) { "https://support.com" }
+ support_link: ->(*) { "https://support.com" },
+ bulk_order_tokens: {
+ sometoken: { customer_id: "bulkcustomer", peer_id: "bulkpeer" },
+ lowtoken: { customer_id: "customerid_low", peer_id: "lowpeer" }
+ }
}.freeze
def panic(e)
diff --git a/test/test_web.rb b/test/test_web.rb
index 85dc91e25a8035c6d6c0e1d62cf35971cac55c2a..8bc2cc36a2a44489fbb0b56437dbb303566b87e3 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -11,6 +11,7 @@ Customer::BLATHER = Minitest::Mock.new
CustomerFwd::BANDWIDTH_VOICE = Minitest::Mock.new
Web::BANDWIDTH_VOICE = Minitest::Mock.new
LowBalance::AutoTopUp::CreditCardSale = Minitest::Mock.new
+Transaction::DB = Minitest::Mock.new
ReachableRedis = Minitest::Mock.new
@@ -38,7 +39,8 @@ class WebTest < Minitest::Test
"catapult_jid-+15551234563" => "customer_customerid_reach@component",
"jmp_customer_jid-customerid2" => "customer2@example.com",
"catapult_jid-+15551230000" => "customer_customerid2@component",
- "jmp_customer_jid-customerid_tombed" => "customer_tombed@example.com"
+ "jmp_customer_jid-customerid_tombed" => "customer_tombed@example.com",
+ "jmp_customer_jid-bulkcustomer" => "bulk@example.com"
),
db: FakeDB.new(
["customerid"] => [{
@@ -75,6 +77,11 @@ class WebTest < Minitest::Test
"balance" => BigDecimal(10),
"plan_name" => "test_usd",
"expires_at" => Time.now + 100
+ }],
+ ["bulkcustomer"] => [{
+ "balance" => BigDecimal(1000),
+ "plan_name" => "test_usd",
+ "expires_at" => Time.now + 100
}]
),
sgx_repo: Bwmsgsv2Repo.new(
@@ -865,4 +872,285 @@ class WebTest < Minitest::Test
assert_mock ReachableRedis
end
em :test_inbound_from_reachability_during_reachability
+
+ def test_bulk_order
+ create_order = stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders"
+ ).to_return(status: 201, body: <<~RESPONSE)
+
+
+ test_order
+
+
+ RESPONSE
+
+ post(
+ "/orders",
+ { quantity: 100 },
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ { id: "test_order" }.to_json,
+ last_response.body
+ )
+ assert_requested create_order
+ end
+ em :test_bulk_order
+
+ def test_bulk_order_low_balance
+ post(
+ "/orders",
+ { quantity: 100 },
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer lowtoken"
+ )
+
+ assert_equal 402, last_response.status
+ end
+ em :test_bulk_order_low_balance
+
+ def test_bulk_order_bad_token
+ post(
+ "/orders",
+ { quantity: 100 },
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer badtoken"
+ )
+
+ assert_equal 401, last_response.status
+ end
+ em :test_bulk_order_bad_token
+
+ def test_order_get
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/testorder"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+ testorder
+ complete
+
+
+ 5551234567
+
+
+
+ RESPONSE
+
+ Transaction::DB.expect(:transaction, nil) do |&blk|
+ blk.call
+ true
+ end
+ Transaction::DB.expect(
+ :exec,
+ nil,
+ [String, Matching.new { |params|
+ assert_equal "bulkcustomer", params[0]
+ assert_equal "testorder", params[1]
+ assert_equal(-1.75, params[4])
+ assert_equal "Bulk order", params[5]
+ }]
+ )
+
+ get(
+ "/orders/testorder",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ { id: "testorder", status: "complete", tels: ["+15551234567"] }.to_json,
+ last_response.body
+ )
+ assert_mock Transaction::DB
+ end
+ em :test_order_get
+
+ def test_order_get_multi
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/testorder"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+ testorder
+ complete
+
+
+ 5551234567
+
+
+ 5551234568
+
+
+
+ RESPONSE
+
+ Transaction::DB.expect(:transaction, nil) do |&blk|
+ blk.call
+ true
+ end
+ Transaction::DB.expect(
+ :exec,
+ nil,
+ [String, Matching.new { |params|
+ assert_equal "bulkcustomer", params[0]
+ assert_equal "testorder", params[1]
+ assert_equal (-1.75 * 2), params[4]
+ assert_equal "Bulk order", params[5]
+ }]
+ )
+
+ get(
+ "/orders/testorder",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ {
+ id: "testorder",
+ status: "complete",
+ tels: ["+15551234567", "+15551234568"]
+ }.to_json,
+ last_response.body
+ )
+ assert_mock Transaction::DB
+ end
+ em :test_order_get_multi
+
+ def test_order_get_received
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/testorder"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+ testorder
+ received
+
+ RESPONSE
+
+ get(
+ "/orders/testorder",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ { id: "testorder", status: "received" }.to_json,
+ last_response.body
+ )
+ end
+ em :test_order_get_received
+
+ def test_order_get_failed
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/testorder"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+ testorder
+ failed
+
+ RESPONSE
+
+ get(
+ "/orders/testorder",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ { id: "testorder", status: "failed" }.to_json,
+ last_response.body
+ )
+ end
+ em :test_order_get_failed
+
+ def test_order_get_bad_token
+ get(
+ "/orders/testorder",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer badtoken"
+ )
+
+ assert_equal 401, last_response.status
+ end
+ em :test_order_get_bad_token
+
+ def test_delete_tel
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/tns/+15551234567/tndetails"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+
+ bulkpeer
+
+
+ RESPONSE
+
+ req = stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//disconnects"
+ ).to_return(status: 200, body: "")
+
+ delete(
+ "/orders/tels/+15551234567",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert last_response.ok?
+ assert_requested req
+ end
+ em :test_delete_tel
+
+ def test_delete_tel_not_yours
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/tns/+15551234567/tndetails"
+ ).to_return(status: 200, body: <<~RESPONSE)
+
+
+ mainpeer
+
+
+ RESPONSE
+
+ delete(
+ "/orders/tels/+15551234567",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer sometoken"
+ )
+
+ assert_equal 401, last_response.status
+ end
+ em :test_delete_tel_not_yours
+
+ def test_delete_tel_bad_token
+ delete(
+ "/orders/tels/+15551234567",
+ "",
+ "HTTP_ACCEPT" => "application/json",
+ "HTTP_AUTHORIZATION" => "Bearer badtoken"
+ )
+
+ assert_equal 401, last_response.status
+ end
+ em :test_delete_tel_bad_token
end
diff --git a/web.rb b/web.rb
index 3e9a835f540c34d5caae9f52721e514235dcafa3..c535f8cdec8533cc66b9da76fa2a64fb9673e9bd 100644
--- a/web.rb
+++ b/web.rb
@@ -9,6 +9,7 @@ require "roda"
require "sentry-ruby"
require "thin"
+require_relative "lib/bandwidth_tn_order"
require_relative "lib/call_attempt_repo"
require_relative "lib/trust_level_repo"
require_relative "lib/cdr"
@@ -503,6 +504,74 @@ class Web < Roda
end
end
+ r.on "orders" do
+ token = r.env["HTTP_AUTHORIZATION"].to_s.sub(/\ABearer\s+/, "")
+
+ r.json do
+ if (orderer = CONFIG[:bulk_order_tokens][token.to_sym])
+ r.get :order_id do |order_id|
+ BandwidthTNOrder.get(order_id).then do |order|
+ if order.status == :complete
+ Transaction.new(
+ customer_id: orderer[:customer_id],
+ transaction_id: order.id,
+ amount: order.tels.length * -1.75,
+ note: "Bulk order",
+ ignore_duplicate: true
+ ).insert.then do
+ {
+ id: order.id,
+ status: order.status,
+ tels: order.tels
+ }.to_json
+ end
+ else
+ { id: order.id, status: order.status }.to_json
+ end
+ end
+ end
+
+ r.on "tels" do
+ r.on :tel, method: :delete do |tel|
+ tn_repo = BandwidthTnRepo.new
+ tn = tn_repo.find(tel)
+ if tn&.dig(:sip_peer, :peer_id).to_s == orderer[:peer_id]
+ tn_repo.disconnect(tel, orderer[:customer_id])
+ { status: "disconnected" }.to_json
+ else
+ response.status = 401
+ { error: "Number not found" }.to_json
+ end
+ end
+ end
+
+ r.post do
+ customer_repo.find(orderer[:customer_id]).then do |customer|
+ if customer.balance >= 1.75 * params["quantity"].to_i
+ BandwidthTNOrder.create_custom(
+ name: "Bulk order",
+ customer_order_id: orderer[:customer_id],
+ peer_id: orderer[:peer_id],
+ state_search_and_order_type: {
+ quantity: params["quantity"].to_i,
+ state: ["CA", "TX", "IL", "NY", "FL"].sample
+ }
+ ).then do |order|
+ { id: order.id }.to_json
+ end
+ else
+ response.status = 402
+ { error: "Balance too low" }.to_json
+ end
+ end
+ end
+ else
+ response.status = 401
+ { error: "Bad token" }.to_json
+ end
+ end
+ end
+
r.public
end
end