~singpolyma/sgx-jmp

a9ebca9a66da1fa0e10bee066a6ba71fe31177a3 — Stephen Paul Weber 9 days ago 51897c8
Usage command

Customer has a CustomerUsage which can fetch data to build a UsageReport
which returns a jabber:x:data form "table" as per spec for per-day in
the last month.

Minutes come from cdr table in postgresql.
Messages come from redis.
6 files changed, 181 insertions(+), 0 deletions(-)

M lib/customer.rb
A lib/customer_usage.rb
A lib/form_table.rb
A lib/usage_report.rb
M sgx_jmp.rb
M test/test_customer.rb
M lib/customer.rb => lib/customer.rb +3 -0
@@ 4,6 4,7 @@ require "forwardable"

require_relative "./blather_ext"
require_relative "./customer_plan"
require_relative "./customer_usage"
require_relative "./backend_sgx"
require_relative "./ibr"
require_relative "./payment_methods"


@@ 47,6 48,7 @@ class Customer
	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
	               :currency, :merchant_account, :plan_name
	def_delegators :@sgx, :register!, :registered?
	def_delegator :@usage, :report, :usage_report

	def initialize(
		customer_id,


@@ 60,6 62,7 @@ class Customer
			plan: plan_name && Plan.for(plan_name),
			expires_at: expires_at
		)
		@usage = CustomerUsage.new(customer_id)
		@customer_id = customer_id
		@balance = balance
		@sgx = sgx

A lib/customer_usage.rb => lib/customer_usage.rb +47 -0
@@ 0,0 1,47 @@
# frozen_string_literal: true

require_relative "./usage_report"

class CustomerUsage
	def initialize(customer_id)
		@customer_id = customer_id
	end

	def report(range)
		EMPromise.all([
			messages_by_day(range),
			minutes_by_day(range)
		]).then do |args|
			UsageReport.new(range, *args)
		end
	end

	def messages_by_day(range)
		EMPromise.all(range.first.downto(range.last).map { |day|
			REDIS.zscore(
				"jmp_customer_outbound_messages-#{@customer_id}",
				day.strftime("%Y%m%d")
			).then { |c| [day, c.to_i] if c }
		}).then { |r| Hash[r.compact].tap { |h| h.default = 0 } }
	end

	QUERY_FOR_MINUTES = <<~SQL
		SELECT
			date_trunc('day', start)::date as day,
			CEIL(SUM(billsec)/60.0)::integer as minutes
		FROM cdr
		WHERE customer_id=$1 and start >= $3 and start < $2
		GROUP BY date_trunc('day', start);
	SQL

	def minutes_by_day(range)
		DB.query_defer(
			QUERY_FOR_MINUTES,
			[@customer_id, range.first, range.last]
		).then do |result|
			result.each_with_object(Hash.new(0)) do |row, minutes|
				minutes[row["day"]] = row["minutes"]
			end
		end
	end
end

A lib/form_table.rb => lib/form_table.rb +34 -0
@@ 0,0 1,34 @@
# frozen_string_literal: true

class FormTable
	def initialize(rows, **cols)
		@cols = cols
		@rows = rows
	end

	def add_to_form(form)
		Nokogiri::XML::Builder.with(form) do |xml|
			xml.reported do
				@cols.each do |var, label|
					xml.field(var: var.to_s, label: label.to_s)
				end
			end

			add_rows_to_xml(xml)
		end
	end

protected

	def add_rows_to_xml(xml)
		@rows.each do |row|
			xml.item do
				row.each.with_index do |val, idx|
					xml.field(var: @cols.keys[idx].to_s) do
						xml.value val.to_s
					end
				end
			end
		end
	end
end

A lib/usage_report.rb => lib/usage_report.rb +44 -0
@@ 0,0 1,44 @@
# frozen_string_literal: true

require_relative "./form_table"

class UsageReport
	def initialize(report_for, messages, minutes)
		@report_for = report_for
		@messages = messages
		@minutes = minutes
	end

	def ==(other)
		report_for == other.report_for &&
			messages == other.messages &&
			minutes == other.minutes
	end

	def form
		form = Blather::Stanza::X.new(:result)
		form.title =
			form.instructions =
				"Usage from #{report_for.first} to #{report_for.last}"
		form_table.add_to_form(form)
		form
	end

	def form_table
		total_messages = 0
		total_minutes = 0

		FormTable.new(
			@report_for.first.downto(@report_for.last).map do |day|
				total_messages += @messages[day]
				total_minutes += @minutes[day]
				[day, @messages[day], @minutes[day]]
			end + [["Total", total_messages, total_minutes]],
			day: "Day", messages: "Messages", minutes: "Minutes"
		)
	end

protected

	attr_reader :report_for, :messages, :minutes
end

M sgx_jmp.rb => sgx_jmp.rb +24 -0
@@ 253,6 253,11 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
			iq.to,
			"jabber:iq:register",
			"Register"
		),
		Blather::Stanza::DiscoItems::Item.new(
			iq.to,
			"usage",
			"Show Monthly Usage"
		)
	]
	self << reply


@@ 316,6 321,25 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "usage", sessionid: nil do |iq|
	sentry_hub = new_sentry_hub(iq, name: iq.node)
	report_for = (Date.today..(Date.today << 1))

	Customer.for_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)

		customer.usage_report(report_for)
	}.then { |usage_report|
		reply = iq.reply
		reply.status = :completed
		reply.command << usage_report.form
		BLATHER << reply
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "web-register", sessionid: nil do |iq|
	sentry_hub = new_sentry_hub(iq, name: iq.node)


M test/test_customer.rb => test/test_customer.rb +29 -0
@@ 8,6 8,8 @@ Customer::BRAINTREE = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
Customer::DB = Minitest::Mock.new
CustomerPlan::DB = Minitest::Mock.new
CustomerUsage::REDIS = Minitest::Mock.new
CustomerUsage::DB = Minitest::Mock.new

class CustomerTest < Minitest::Test
	def test_for_jid


@@ 176,4 178,31 @@ class CustomerTest < Minitest::Test
		Customer.new("test").stanza_from(m)
		Customer::BLATHER.verify
	end

	def test_customer_usage_report
		report_for = (Date.today..(Date.today - 1))
		report_for.first.downto(report_for.last).each.with_index do |day, idx|
			CustomerUsage::REDIS.expect(
				:zscore,
				EMPromise.resolve(idx),
				["jmp_customer_outbound_messages-test", day.strftime("%Y%m%d")]
			)
		end
		CustomerUsage::DB.expect(
			:query_defer,
			EMPromise.resolve([{ "day" => report_for.first, "minutes" => 123 }]),
			[String, ["test", report_for.first, report_for.last]]
		)
		assert_equal(
			UsageReport.new(
				report_for, {
					Date.today => 0,
					(Date.today - 1) => 1
				},
				Date.today => 123
			),
			Customer.new("test").usage_report(report_for).sync
		)
	end
	em :test_customer_usage_report
end