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