~singpolyma/sgx-jmp

e5730b67e2858b62607a34f4f9669bf7435dce81 — Stephen Paul Weber a month ago ef6cc23 master
Currency aware

Look up the user's plan to find out what currency to charge them in.
3 files changed, 41 insertions(+), 23 deletions(-)

M config.dhall.sample
M schemas
M sgx_jmp.rb
M config.dhall.sample => config.dhall.sample +7 -2
@@ 17,6 17,11 @@
		environment = "sandbox",
		merchant_id = "",
		public_key = "",
		private_key = ""
	}
		private_key = "",
		merchant_accounts = {
			USD = "",
			CAD = ""
		}
	},
	plans = ./plans.dhall
}

M schemas => schemas +1 -1
@@ 1,1 1,1 @@
Subproject commit b0729aba768a943ed9f695d1468f1c62f2076727
Subproject commit e005a4d6b09636d21614be0c513ce9360cef2ccb

M sgx_jmp.rb => sgx_jmp.rb +33 -20
@@ 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|