Flatten main SIM pricing config

Amolith created

Replace the main app's SIM pricing maps with fixed CAD/USD records for
SIM, eSIM, per-GB refill, and annual prices.

Derive the displayed plan label from the per-GB and annual prices so the
one-time SIM prices and ongoing plan pricing are not duplicated in
config.

Change summary

config-schema.dhall             | 16 ++++------------
config.dhall.sample             | 12 ++++--------
lib/sim_kind.rb                 | 23 ++++++++++++++---------
lib/sim_pricing.rb              | 28 +++++++++++++++++++++++++++-
sgx_jmp.rb                      |  6 ++----
test/test_helper.rb             | 18 ++++++++++++------
test/test_monthly_data_limit.rb | 15 ++++++++++++++-
7 files changed, 77 insertions(+), 41 deletions(-)

Detailed changes

config-schema.dhall 🔗

@@ -81,18 +81,10 @@
       }
 , simpleswap_api_key : Text
 , sims :
-    { CAD : { per_gb : Natural }
-    , USD : { per_gb : Natural }
-    , esim :
-        List
-          { mapKey : < CAD | USD >
-          , mapValue : { plan : Text, price : Natural }
-          }
-    , sim :
-        List
-          { mapKey : < CAD | USD >
-          , mapValue : { plan : Text, price : Natural }
-          }
+    { annual : { CAD : Natural, USD : Natural }
+    , esim : { CAD : Natural, USD : Natural }
+    , per_gb : { CAD : Natural, USD : Natural }
+    , sim : { CAD : Natural, USD : Natural }
     }
 , sip : { app : Text, realm : Text }
 , sip_host : Text

config.dhall.sample 🔗

@@ -67,14 +67,10 @@ in
 		}
 	],
 	sims = {
-		CAD = { per_gb = 100 },
-		USD = { per_gb = 100 },
-		sim = [
-			{ mapKey = <CAD|USD>.CAD, mapValue = { price = 1, plan = "$1 / GB + $1 / year" } }
-		],
-		esim = [
-			{ mapKey = <CAD|USD>.CAD, mapValue = { price = 1, plan = "$1 / GB + $1 / year" } }
-		]
+		annual = { CAD = 100, USD = 100 },
+		esim = { CAD = 1, USD = 1 },
+		per_gb = { CAD = 100, USD = 100 },
+		sim = { CAD = 1, USD = 1 }
 	},
 	electrum = {
 		rpc_uri = "",

lib/sim_kind.rb 🔗

@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative "sim_pricing"
+
 class SIMKind
 	def initialize(variant)
 		@klass =
@@ -19,13 +21,16 @@ class SIMKind
 	end
 
 	# @param [Customer] customer
-	# @raise [ArgumentError] If matching `CONFIG[:sims]`
-	# 	key not found for `@variant` and customer
+	# @raise [ArgumentError] If matching SIM price is missing from `CONFIG[:sims]`
 	def get_processor(customer)
-		cfg = cfg(customer.currency)
-		raise ArgumentError, "Invalid config" unless cfg
-
-		@klass.for(customer, **cfg)
+		price = price_cents(customer.currency)
+		raise ArgumentError, "Invalid config" unless price
+
+		@klass.for(
+			customer,
+			price: price,
+			plan: SIMPricing.plan_label(customer.currency, CONFIG)
+		)
 	end
 
 	# @return [String] The result of either
@@ -38,15 +43,15 @@ class SIMKind
 	# @return [Float, NilClass] Returns nil if `customer.currency`
 	# 	is nil, or if @variant is malformed.
 	def price(customer)
-		cfg = cfg(customer.currency || :USD)
-		cfg[:price] / 100.to_d
+		price = price_cents(customer.currency || :USD)
+		price / 100.to_d if price
 	end
 
 protected
 
 	# @param [Symbol] currency One of the currencies from
 	# 	CONFIG[:plans] entries
-	def cfg(currency)
+	def price_cents(currency)
 		CONFIG.dig(:sims, @variant.to_sym, currency)
 	end
 end

lib/sim_pricing.rb 🔗

@@ -5,8 +5,34 @@ require "bigdecimal"
 module SIMPricing
 	# SIM refill billing records currency amounts in transactions. Use BigDecimal
 	# because the configured per-GB price is stored in cents.
+	def self.per_gb_price(currency, config)
+		configured_price_dollars(:per_gb, currency, config)
+	end
+
+	def self.annual_price(currency, config)
+		configured_price_dollars(:annual, currency, config)
+	end
+
+	def self.configured_price_dollars(price_type, currency, config)
+		BigDecimal(
+			config.fetch(:sims).fetch(price_type).fetch(currency)
+		) / 100
+	end
+	private_class_method :configured_price_dollars
+
 	def self.five_gb_refill_price(currency, config)
-		(BigDecimal(config[:sims][currency][:per_gb]) / 100) * 5
+		per_gb_price(currency, config) * 5
+	end
+
+	def self.per_gb_label(currency, config)
+		"$%.2f / GB" % per_gb_price(currency, config)
+	end
+
+	def self.plan_label(currency, config)
+		"$%.2f / GB + $%.2f / year" % [
+			per_gb_price(currency, config),
+			annual_price(currency, config)
+		]
 	end
 
 	# CustomerRepo#put_monthly_limits stores limits with to_i, so render monthly

sgx_jmp.rb 🔗

@@ -783,11 +783,9 @@ Command.new(
 		}.then { |iq|
 			case iq.form.field("http://jabber.org/protocol/commands#actions")&.value
 			when "order-sim"
-				SIMOrder.for(customer, **CONFIG.dig(:sims, :sim, customer.currency))
+				SIMKind.new("sim").get_processor(customer)
 			when "order-esim"
-				SIMOrder::ESIM.for(
-					customer, **CONFIG.dig(:sims, :esim, customer.currency)
-				)
+				SIMKind.new("esim").get_processor(customer)
 			when "edit-nicknames"
 				EditSimNicknames.new(customer, sims)
 			else

test/test_helper.rb 🔗

@@ -161,15 +161,21 @@ CONFIG = {
 	electrum_notify_url: ->(*) { "http://notify.example.com" },
 	admin_notify: "admin_room@example.com",
 	sims: {
-		USD: { per_gb: 100 },
-		CAD: { per_gb: 120 },
+		annual: {
+			USD: 100,
+			CAD: 120
+		},
+		per_gb: {
+			USD: 100,
+			CAD: 120
+		},
 		sim: {
-			USD: { price: 500, plan: "1GB" },
-			CAD: { price: 600, plan: "1GB" }
+			USD: 500,
+			CAD: 600
 		},
 		esim: {
-			USD: { price: 300, plan: "500MB" },
-			CAD: { price: 400, plan: "500MB" }
+			USD: 300,
+			CAD: 400
 		}
 	},
 	keep_area_codes: [

test/test_monthly_data_limit.rb 🔗

@@ -63,13 +63,26 @@ class MonthlyDataLimitTest < Minitest::Test
 		assert_equal 60, limits[:max]
 	end
 
+	def test_formats_refill_price_for_display
+		assert_equal "$1.20 / GB", SIMPricing.per_gb_label(:CAD, CONFIG)
+	end
+
+	def test_formats_plan_price_for_display
+		assert_equal(
+			"$1.20 / GB + $1.20 / year",
+			SIMPricing.plan_label(:CAD, CONFIG)
+		)
+	end
+
 	def test_does_not_need_sim_pricing_without_sims
 		assert_equal({}, MonthlyDataLimit.for_sims([], :CAD, CONFIG))
 	end
 
 	def test_uses_integer_dollars_for_fractional_refill_price
 		config = CONFIG.merge(
-			sims: CONFIG[:sims].merge(CAD: { per_gb: 125 })
+			sims: CONFIG[:sims].merge(
+				per_gb: CONFIG[:sims][:per_gb].merge(CAD: 125)
+			)
 		)
 		limits = MonthlyDataLimit.for_currency(:CAD, config)