Currency aware

Stephen Paul Weber created

Look up the user's plan to find out what currency to charge them in.

Change summary

config.dhall.sample |  9 ++++++-
schemas             |  2 
sgx_jmp.rb          | 53 +++++++++++++++++++++++++++++-----------------
3 files changed, 41 insertions(+), 23 deletions(-)

Detailed changes

config.dhall.sample 🔗

@@ -17,6 +17,11 @@
 		environment = "sandbox",
 		merchant_id = "",
 		public_key = "",
-		private_key = ""
-	}
+		private_key = "",
+		merchant_accounts = {
+			USD = "",
+			CAD = ""
+		}
+	},
+	plans = ./plans.dhall
 }

schemas 🔗

@@ -1 +1 @@
-Subproject commit b0729aba768a943ed9f695d1468f1c62f2076727
+Subproject commit e005a4d6b09636d21614be0c513ce9360cef2ccb

sgx_jmp.rb 🔗

@@ -9,11 +9,14 @@ require "em-hiredis"
 require "em_promise"
 require "time-hash"
 
-CONFIG = Dhall::Coder.load(ARGV[0])
+CONFIG =
+	Dhall::Coder
+	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
+	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
 
 # Braintree is not async, so wrap in EM.defer for now
 class AsyncBraintree
-	def initialize(environment:, merchant_id:, public_key:, private_key:)
+	def initialize(environment:, merchant_id:, public_key:, private_key:, **)
 		@gateway = Braintree::Gateway.new(
 			environment: environment,
 			merchant_id: merchant_id,
@@ -50,7 +53,7 @@ class AsyncBraintree
 	end
 end
 
-BRAINTREE = AsyncBraintree.new(**CONFIG["braintree"].transform_keys(&:to_sym))
+BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
 
 def node(name, parent, ns: nil)
 	Niceogiri::XML::Node.new(
@@ -94,7 +97,7 @@ end
 def proxy_jid(jid)
 	Blather::JID.new(
 		escape_jid(jid.stripped),
-		CONFIG["component"]["jid"],
+		CONFIG[:component][:jid],
 		jid.resource
 	)
 end
@@ -162,18 +165,18 @@ when_ready do
 	DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
 
 	EM.add_periodic_timer(3600) do
-		ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG["server"]["host"])
-		ping.from = CONFIG["component"]["jid"]
+		ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG[:server][:host])
+		ping.from = CONFIG[:component][:jid]
 		self << ping
 	end
 end
 
 # workqueue_count MUST be 0 or else Blather uses threads!
 setup(
-	CONFIG["component"]["jid"],
-	CONFIG["component"]["secret"],
-	CONFIG["server"]["host"],
-	CONFIG["server"]["port"],
+	CONFIG[:component][:jid],
+	CONFIG[:component][:secret],
+	CONFIG[:server][:host],
+	CONFIG[:server][:port],
 	nil,
 	nil,
 	workqueue_count: 0
@@ -186,7 +189,7 @@ end
 ibr :get? do |iq|
 	fwd = iq.dup
 	fwd.from = proxy_jid(iq.from)
-	fwd.to = Blather::JID.new(nil, CONFIG["sgx"], iq.to.resource)
+	fwd.to = Blather::JID.new(nil, CONFIG[:sgx], iq.to.resource)
 	fwd.id = "JMPGET%#{iq.id}"
 	self << fwd
 end
@@ -205,7 +208,7 @@ ibr :result? do |iq|
 	reply.id = iq.id.sub(/JMP[GS]ET%/, "")
 	reply.from = Blather::JID.new(
 		nil,
-		CONFIG["component"]["jid"],
+		CONFIG[:component][:jid],
 		iq.from.resource
 	)
 	reply.to = unproxy_jid(iq.to)
@@ -217,7 +220,7 @@ ibr :error? do |iq|
 	reply.id = iq.id.sub(/JMP[GS]ET%/, "")
 	reply.from = Blather::JID.new(
 		nil,
-		CONFIG["component"]["jid"],
+		CONFIG[:component][:jid],
 		iq.from.resource
 	)
 	reply.to = unproxy_jid(iq.to)
@@ -226,11 +229,11 @@ end
 
 ibr :set? do |iq|
 	fwd = iq.dup
-	CONFIG["creds"].each do |k, v|
+	CONFIG[:creds].each do |k, v|
 		fwd.public_send("#{k}=", v)
 	end
 	fwd.from = proxy_jid(iq.from)
-	fwd.to = Blather::JID.new(nil, CONFIG["sgx"], iq.to.resource)
+	fwd.to = Blather::JID.new(nil, CONFIG[:sgx], iq.to.resource)
 	fwd.id = "JMPSET%#{iq.id}"
 	self << fwd
 end
@@ -289,6 +292,7 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
 	reply = iq.reply
 	reply.items = [
 		# TODO: don't show this item if no braintree methods available
+		# TODO: don't show this item if no plan for this customer
 		Blather::Stanza::DiscoItems::Item.new(
 			iq.to,
 			"buy-credit",
@@ -310,16 +314,23 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
 
 		EMPromise.all([
 			DB.query_defer(
-				"SELECT balance FROM balances WHERE customer_id=$1 LIMIT 1",
+				"SELECT COALESCE(balance,0) AS balance, plan_name FROM " \
+				"balances LEFT JOIN customer_plans USING (customer_id) " \
+				"WHERE customer_id=$1 LIMIT 1",
 				[customer_id]
 			).then do |rows|
-				rows.first&.dig("balance") || BigDecimal.new(0)
+				rows.first || { "balance" => BigDecimal.new(0) }
 			end,
 			BRAINTREE.customer.find(customer_id).payment_methods
 		])
-	}.then { |(balance, payment_methods)|
+	}.then { |(row, payment_methods)|
 		raise "No payment methods available" if payment_methods.empty?
 
+		plan = CONFIG[:plans].find { |p| p[:name] == row["plan_name"] }
+		raise "No plan for this customer" unless plan
+		merchant_account = CONFIG[:braintree][:merchant_accounts][plan[:currency]]
+		raise "No merchant account for this currency" unless merchant_account
+
 		default_payment_method = payment_methods.index(&:default?)
 
 		form = reply.form
@@ -328,7 +339,7 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
 		form.fields = [
 			{
 				type: "fixed",
-				value: "Current balance: $#{'%.2f' % balance}"
+				value: "Current balance: $#{'%.2f' % row['balance']}"
 			},
 			if payment_methods.length > 1
 				{
@@ -356,9 +367,10 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
 
 		EMPromise.all([
 			payment_methods,
+			merchant_account,
 			command_reply_and_promise(reply)
 		])
-	}.then { |(payment_methods, iq2)|
+	}.then { |(payment_methods, merchant_account, iq2)|
 		iq = iq2 # This allows the catch to use it also
 		payment_method = payment_methods.fetch(
 			iq.form.field("payment_method")&.value.to_i
@@ -366,6 +378,7 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
 		BRAINTREE.transaction.sale(
 			amount: iq.form.field("amount").value.to_s,
 			payment_method_token: payment_method.token,
+			merchant_account_id: merchant_account,
 			options: { submit_for_settlement: true }
 		)
 	}.then { |braintree_response|