Allow searching for tel by offer code

Stephen Paul Weber created

Also redeem the invite at the time if found. HACK: always assumes USD plan

Change summary

config-schema.dhall         |  1 
config.dhall.sample         |  1 
lib/tel_selections.rb       | 31 +++++++++++++++++
test/test_helper.rb         | 23 +++++++++++-
test/test_tel_selections.rb | 69 +++++++++++++++++++++++++++++++++++++++
5 files changed, 123 insertions(+), 2 deletions(-)

Detailed changes

config-schema.dhall 🔗

@@ -43,6 +43,7 @@
 , keepgo : Optional { access_token : Text, api_key : Text }
 , notify_admin : Text
 , notify_from : Text
+, offer_codes : List { mapKey : Text, mapValue : Text }
 , ogm_path : Text
 , ogm_web_root : Text
 , onboarding_domain : Text

config.dhall.sample 🔗

@@ -110,6 +110,7 @@ in
 	upstream_domain = "example.net",
 	approved_domains = toMap { `example.com` = Some "customer_id" },
 	parented_domains = toMap { `example.com` = { customer_id = "customer_id", plan_name = "usd" } },
+	offer_codes = toMap { someone = "xmpp:thing" },
 	keepgo = Some { api_key = "", access_token = "" },
 	simpleswap_api_key = "",
 	reachability_senders = [ "+14445556666" ],

lib/tel_selections.rb 🔗

@@ -440,6 +440,37 @@ class TelSelections
 				[]
 			end
 
+			class OfferCode < Q
+				Q.register(/\A[0-9A-F]{8}\Z/) { |q, **kw| self.for(q, **kw) }
+
+				def self.for(q, customer:, redis:, db:, **)
+					InvitesRepo.new(db, redis)
+						.claim_code(customer.customer_id, q) { |claimed|
+							# HACK: assume USD plan for all offer code claims
+							customer.with_plan("USD").activate_plan_starting_now
+							claimed
+						}.then { |claimed|
+							if (source = CONFIG[:offer_codes][claimed&.dig("creator_id")])
+								new(source)
+							end
+						}.catch_only(InvitesRepo::Invalid) {}
+				end
+
+				def initialize(source)
+					@source = source
+				end
+
+				def iris_query; end
+
+				def sql_query
+					[
+						"SELECT * FROM tel_inventory " \
+						"WHERE available_after < LOCALTIMESTAMP AND source=$1",
+						@source
+					]
+				end
+			end
+
 			{
 				areaCode: [:AreaCode, /\A[2-9][0-9]{2}\Z/],
 				npaNxx: [:NpaNxx, /\A(?:[2-9][0-9]{2}){2}\Z/],

test/test_helper.rb 🔗

@@ -107,6 +107,14 @@ CONFIG = {
 			minutes: { included: 10440, price: 87 },
 			allow_register: true
 		},
+		{
+			name: "USD",
+			currency: :USD,
+			monthly_price: 10000,
+			messages: :unlimited,
+			minutes: { included: 10440, price: 87 },
+			allow_register: true
+		},
 		{
 			name: "test_bad_currency",
 			currency: :BAD
@@ -166,6 +174,9 @@ CONFIG = {
 			plan_name: "test_usd"
 		}
 	},
+	offer_codes: {
+		"pplus" => "xmpp:pplus"
+	},
 	bandwidth_site: "test_site",
 	bandwidth_peer: "test_peer",
 	keepgo: { api_key: "keepgokey", access_token: "keepgotoken" },
@@ -389,8 +400,16 @@ class FakeDB
 		@items = items
 	end
 
-	def query_defer(_, args)
-		EMPromise.resolve(@items.fetch(args, []).to_a)
+	def transaction
+		yield
+	end
+
+	def exec(_, args)
+		@items.fetch(args, []).to_a
+	end
+
+	def query_defer(sql, args)
+		EMPromise.resolve(exec(sql, args))
 	end
 
 	def query_one(_, *args, field_names_as: :symbol, default: nil)

test/test_tel_selections.rb 🔗

@@ -503,5 +503,74 @@ class TelSelectionsTest < Minitest::Test
 			assert_raises { TelSelections::ChooseTel::Q.for("garbage").sync }
 		end
 		em :test_for_garbage
+
+		def test_offer_code
+			CustomerPlan::DB.expect(
+				:exec,
+				OpenStruct.new(cmd_tuples: 1),
+				[String, ["test", "USD", nil]]
+			)
+			CustomerPlan::DB.expect(
+				:exec,
+				OpenStruct.new(cmd_tuples: 0),
+				[String, ["test"]]
+			)
+			db = FakeDB.new(
+				["test", "DEADBEEF"] => [{ "creator_id" => "pplus" }]
+			)
+			q = TelSelections::ChooseTel::Q.for(
+				"DEADBEEF",
+				customer: customer,
+				redis: FakeRedis.new, db: db, memcache: FakeMemcache.new
+			).sync
+			assert_equal(
+				[
+					"SELECT * FROM tel_inventory " \
+					"WHERE available_after < LOCALTIMESTAMP AND source=$1",
+					"xmpp:pplus"
+				],
+				q.sql_query
+			)
+			assert_mock CustomerPlan::DB
+		end
+		em :test_offer_code
+
+		def test_offer_code_invalid
+			db = FakeDB.new
+			assert_raises do
+				q = TelSelections::ChooseTel::Q.for(
+					"DEADBEEF",
+					customer: customer,
+					redis: FakeRedis.new, db: db, memcache: FakeMemcache.new
+				).sync
+			end
+			assert_mock CustomerPlan::DB
+		end
+		em :test_offer_code_invalid
+
+		def test_offer_code_just_invite
+			CustomerPlan::DB.expect(
+				:exec,
+				OpenStruct.new(cmd_tuples: 1),
+				[String, ["test", "USD", nil]]
+			)
+			CustomerPlan::DB.expect(
+				:exec,
+				OpenStruct.new(cmd_tuples: 0),
+				[String, ["test"]]
+			)
+			db = FakeDB.new(
+				["test", "DEADBEEF"] => [{ "creator_id" => "notpplus" }]
+			)
+			assert_raises do
+				q = TelSelections::ChooseTel::Q.for(
+					"DEADBEEF",
+					customer: customer,
+					redis: FakeRedis.new, db: db, memcache: FakeMemcache.new
+				).sync
+			end
+			assert_mock CustomerPlan::DB
+		end
+		em :test_offer_code_just_invite
 	end
 end