M Gemfile => Gemfile +1 -0
@@ 18,6 18,7 @@ gem "money-open-exchange-rates"
gem "multibases"
gem "multihashes"
gem "ougai"
+gem "relative_time"
gem "roda"
gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "sip_credential"
gem "sentry-ruby", "<= 4.3.1"
A forms/admin_info.rb => forms/admin_info.rb +53 -0
@@ 0,0 1,53 @@
+result!
+title "Customer Info"
+
+render "customer_info_partial", info: @admin_info.info
+
+render @admin_info.info.plan_info.admin_template
+
+field(
+ var: "jid",
+ label: "JID",
+ value: @admin_info.jid.unproxied.to_s
+)
+
+field(
+ var: "cheo_jid",
+ label: "Cheo JID",
+ value: @admin_info.jid.to_s
+)
+
+field(
+ var: "customer_id",
+ label: "Customer ID",
+ value: @admin_info.customer_id
+)
+
+if @admin_info.fwd.uri
+ field(
+ var: "fwd",
+ label: "Fwd",
+ value: @admin_info.fwd.uri
+ )
+
+ field(
+ var: "fwd_timeout",
+ label: "Fwd Timeout",
+ value: @admin_info.fwd.timeout.to_s
+ )
+end
+
+field(
+ var: "api",
+ label: "API",
+ value: @admin_info.api.to_s
+)
+
+if @admin_info.info.tel
+ field(
+ var: "link",
+ label: "Link",
+ type: "jid-single",
+ value: "#{@admin_info.info.tel}@#{CONFIG[:upstream_domain]}"
+ )
+end
A forms/admin_plan_info.rb => forms/admin_plan_info.rb +6 -0
@@ 0,0 1,6 @@
+field(
+ var: "start_date",
+ label: "Customer since",
+ description: @plan_info.relative_start_date,
+ value: @plan_info.formatted_start_date
+)
A forms/customer_info.rb => forms/customer_info.rb +4 -0
@@ 0,0 1,4 @@
+result!
+title "Account Info"
+
+render "customer_info_partial"
A forms/customer_info_partial.rb => forms/customer_info_partial.rb +29 -0
@@ 0,0 1,29 @@
+field(
+ var: "account_status",
+ label: "Account Status",
+ value: @info.plan_info.status
+)
+
+if @info.tel
+ field(
+ var: "tel",
+ label: "Phone Number",
+ value: @info.tel
+ )
+end
+
+if @info.cnam
+ field(
+ var: "lidb_name",
+ label: "CNAM",
+ value: @info.cnam
+ )
+end
+
+field(
+ var: "balance",
+ label: "Balance",
+ value: "$%.4f" % @info.balance
+)
+
+render @info.plan_info.template
A forms/customer_picker.rb => forms/customer_picker.rb +14 -0
@@ 0,0 1,14 @@
+form!
+title "Pick Customer"
+
+instructions(
+ "Tell us something about the customer and we'll try to get more " \
+ "information for you"
+)
+
+field(
+ var: "q",
+ type: "text-single",
+ label: "Something about the customer",
+ description: "Supported things include: customer ID, JID, phone number"
+)
A forms/legacy_customer_admin_info.rb => forms/legacy_customer_admin_info.rb +29 -0
@@ 0,0 1,29 @@
+result!
+title "Customer Info"
+
+render "legacy_customer_info_partial", info: @admin_info.info
+
+field(
+ var: "jid",
+ label: "JID",
+ value: @admin_info.jid.unproxied.to_s
+)
+
+field(
+ var: "cheo_jid",
+ label: "Cheo JID",
+ value: @admin_info.jid.to_s
+)
+
+field(
+ var: "api",
+ label: "API",
+ value: @admin_info.api.to_s
+)
+
+field(
+ var: "link",
+ label: "Link",
+ type: "jid-single",
+ value: "#{@admin_info.info.tel}@#{CONFIG[:upstream_domain]}"
+)
A forms/legacy_customer_info.rb => forms/legacy_customer_info.rb +4 -0
@@ 0,0 1,4 @@
+result!
+title "Account Info"
+
+render "legacy_customer_info_partial"
A forms/legacy_customer_info_partial.rb => forms/legacy_customer_info_partial.rb +11 -0
@@ 0,0 1,11 @@
+field(
+ var: "account_status",
+ label: "Account Status",
+ value: "Legacy"
+)
+
+field(
+ var: "tel",
+ label: "Phone Number",
+ value: @info.tel
+)
A forms/no_customer_info.rb => forms/no_customer_info.rb +8 -0
@@ 0,0 1,8 @@
+result!
+title "Customer Info"
+
+field(
+ var: "account_status",
+ label: "Account Status",
+ value: "Not Found"
+)
A forms/plan_info.rb => forms/plan_info.rb +38 -0
@@ 0,0 1,38 @@
+if @admin_info
+ field(
+ var: "plan",
+ label: "Plan",
+ value: @plan_info.plan.plan_name
+ )
+end
+
+field(
+ var: "renewal",
+ label: "Renewal",
+ value: @plan_info.monthly_price
+)
+
+field(
+ var: "currency",
+ label: "Currency",
+ value: @plan_info.currency
+)
+
+field(
+ var: "expires_at",
+ label: @plan_info.plan.active? ? "Next renewal" : "Expired at",
+ value: @plan_info.expires_at.strftime("%Y-%m-%d")
+)
+
+if @plan_info.auto_top_up_amount.positive?
+ field(
+ var: "auto_top_up_amount",
+ label: "Auto Top-up",
+ value: @plan_info.auto_top_up_amount.to_s
+ )
+else
+ field(
+ label: "Auto Top-up",
+ value: "No"
+ )
+end
M lib/customer.rb => lib/customer.rb +2 -2
@@ 129,11 129,11 @@ class Customer
end
def admin_info
- AdminInfo.for(self, @plan, expires_at)
+ AdminInfo.for(self, @plan)
end
def info
- CustomerInfo.for(self, @plan, expires_at)
+ CustomerInfo.for(self, @plan)
end
protected def_delegator :@plan, :expires_at
M lib/customer_fwd.rb => lib/customer_fwd.rb +4 -0
@@ 32,6 32,10 @@ class CustomerFwd
def to_i
@timeout
end
+
+ def to_s
+ to_i.to_s
+ end
end
value_semantics do
M lib/customer_info.rb => lib/customer_info.rb +79 -61
@@ 1,78 1,101 @@
# frozen_string_literal: true
require "bigdecimal"
+require "relative_time"
require "value_semantics/monkey_patched"
require_relative "proxied_jid"
require_relative "customer_plan"
+require_relative "form_template"
-class CustomerInfo
- value_semantics do
- plan CustomerPlan
- auto_top_up_amount Integer
- tel Either(String, nil)
- balance BigDecimal
- expires_at Either(Time, nil)
- cnam Either(String, nil)
- end
+class PlanInfo
+ def self.for(plan)
+ return EMPromise.resolve(NoPlan.new) unless plan&.plan_name
- def self.for(customer, plan, expires_at)
- plan.auto_top_up_amount.then do |auto_top_up_amount|
+ EMPromise.all([
+ plan.activation_date,
+ plan.auto_top_up_amount
+ ]).then do |adate, atua|
new(
- plan: plan,
- auto_top_up_amount: auto_top_up_amount,
- tel: customer.registered? ? customer.registered?.phone : nil,
- balance: customer.balance,
- expires_at: expires_at,
- cnam: customer.tndetails&.dig(:features, :lidb, :subscriber_information)
+ plan: plan, start_date: adate,
+ auto_top_up_amount: atua
)
end
end
- def account_status
- if plan.plan_name.nil?
+ class NoPlan
+ def template
+ FormTemplate.new("")
+ end
+
+ def admin_template
+ FormTemplate.new("")
+ end
+
+ def status
"Transitional"
- elsif plan.active?
- "Active"
- else
- "Expired"
end
end
- def next_renewal
- return unless expires_at
+ value_semantics do
+ plan CustomerPlan
+ start_date Time
+ auto_top_up_amount Integer
+ end
- { var: "Next renewal", value: expires_at.strftime("%Y-%m-%d") }
+ def expires_at
+ plan.expires_at
end
- def monthly_amount
- return unless plan.monthly_price
+ def template
+ FormTemplate.for("plan_info", plan_info: self)
+ end
+
+ def admin_template
+ FormTemplate.for("admin_plan_info", plan_info: self)
+ end
- { var: "Renewal", value: "$%.4f / month" % plan.monthly_price }
+ def monthly_price
+ "$%.4f / month" % plan.monthly_price
end
- def auto_top_up
- {
- label: "Auto Top-up"
- }.merge(
- if auto_top_up_amount.positive?
- { value: auto_top_up_amount.to_s, var: "auto_top_up_amount" }
- else
- { value: "No" }
- end
- )
+ def relative_start_date
+ RelativeTime.in_words(start_date)
+ end
+
+ def formatted_start_date
+ start_date.strftime("%Y-%m-%d %H:%M:%S")
+ end
+
+ def currency
+ (plan.currency || "No Currency").to_s
+ end
+
+ def status
+ plan.active? ? "Active" : "Expired"
+ end
+end
+
+class CustomerInfo
+ value_semantics do
+ plan_info Either(PlanInfo, PlanInfo::NoPlan)
+ tel Either(String, nil)
+ balance BigDecimal
+ cnam Either(String, nil)
+ end
+
+ def self.for(customer, plan)
+ PlanInfo.for(plan).then do |plan_info|
+ new(
+ plan_info: plan_info,
+ tel: customer.registered? ? customer.registered?.phone : nil,
+ balance: customer.balance,
+ cnam: customer.tndetails&.dig(:features, :lidb, :subscriber_information)
+ )
+ end
end
- def fields
- [
- { var: "Account Status", value: account_status },
- ({ var: "tel", label: "Phone Number", value: tel } if tel),
- ({ var: "lidb_name", label: "CNAM", value: cnam } if cnam),
- { var: "Balance", value: "$%.4f" % balance },
- monthly_amount,
- next_renewal,
- auto_top_up,
- { var: "Currency", value: (plan.currency || "No Currency").to_s }
- ].compact
+ def form
+ FormTemplate.render("customer_info", info: self)
end
end
@@ 80,30 103,25 @@ class AdminInfo
value_semantics do
jid ProxiedJID, coerce: ProxiedJID.method(:new)
customer_id String
+ fwd Either(CustomerFwd, nil)
info CustomerInfo
api API
end
- def self.for(customer, plan, expires_at)
+ def self.for(customer, plan)
EMPromise.all([
- CustomerInfo.for(customer, plan, expires_at),
+ CustomerInfo.for(customer, plan),
customer.api
]).then do |info, api_value|
new(
jid: customer.jid,
customer_id: customer.customer_id,
- info: info, api: api_value
+ fwd: customer.fwd, info: info, api: api_value
)
end
end
- def fields
- info.fields + [
- { var: "JID", value: jid.unproxied.to_s },
- { var: "Cheo JID", value: jid.to_s },
- { var: "Customer ID", value: customer_id },
- { var: "Plan", value: info.plan.plan_name || "No Plan" },
- { var: "API", value: api.to_s }
- ]
+ def form
+ FormTemplate.render("admin_info", admin_info: self)
end
end
M lib/customer_info_form.rb => lib/customer_info_form.rb +3 -5
@@ 30,14 30,12 @@ class CustomerInfoForm
end
class NoCustomer
- class AdminInfo
- def fields
- [{ var: "Account Status", value: "Not Found" }]
- end
+ def form
+ FormTemplate.render("no_customer_info")
end
def admin_info
- AdminInfo.new
+ self
end
end
M lib/customer_plan.rb => lib/customer_plan.rb +12 -0
@@ 52,6 52,18 @@ class CustomerPlan
SQL
end
+ def activation_date
+ dates = DB.query_defer(<<~SQL, [@customer_id])
+ SELECT
+ MIN(LOWER(date_range)) AS start_date
+ FROM plan_log WHERE customer_id = $1;
+ SQL
+
+ dates.then do |r|
+ r.first["start_date"]
+ end
+ end
+
protected
def charge_for_plan
M lib/form_template.rb => lib/form_template.rb +68 -4
@@ 10,13 10,23 @@ class FormTemplate
freeze
end
- def self.render(path, **kwargs)
+ def self.for(path, **kwargs)
+ if path.is_a?(FormTemplate)
+ raise "Sent args and a FormTemplate" unless kwargs.empty?
+
+ return path
+ end
+
full_path = File.dirname(__dir__) + "/forms/#{path}.rb"
- new(File.read(full_path), full_path, **kwargs).render
+ new(File.read(full_path), full_path, **kwargs)
end
- def render(**kwargs)
- one = OneRender.new(**@args.merge(kwargs))
+ def self.render(path, context=OneRender.new, **kwargs)
+ self.for(path).render(context, **kwargs)
+ end
+
+ def render(context=OneRender.new, **kwargs)
+ one = context.merge(**@args).merge(**kwargs)
one.instance_eval(@template, @filename)
one.form
end
@@ 30,6 40,10 @@ class FormTemplate
@__builder = Nokogiri::XML::Builder.with(@__form)
end
+ def merge(**kwargs)
+ OneRender.new(**to_h.merge(kwargs))
+ end
+
def form!
@__type_set = true
@__form.type = :form
@@ 74,6 88,23 @@ class FormTemplate
@__form.fields += [f]
end
+ def context
+ PartialRender.new(@__form, @__builder, **to_h)
+ end
+
+ def render(path, **kwargs)
+ FormTemplate.render(path, context, **kwargs)
+ end
+
+ def to_h
+ instance_variables
+ .reject { |sym| sym.to_s.start_with?("@__") }
+ .each_with_object({}) { |var, acc|
+ name = var.to_s[1..-1]
+ acc[name.to_sym] = instance_variable_get(var)
+ }
+ end
+
def xml
@__builder
end
@@ 83,5 114,38 @@ class FormTemplate
@__form
end
+
+ class PartialRender < OneRender
+ def initialize(form, builder, **kwargs)
+ kwargs.each do |k, v|
+ instance_variable_set("@#{k}", v)
+ end
+ @__form = form
+ @__builder = builder
+ end
+
+ def merge(**kwargs)
+ PartialRender.new(@__form, @__builder, **to_h.merge(kwargs))
+ end
+
+ # As a partial, we are not a complete form
+ def form; end
+
+ def form!
+ raise "Invalid 'form!' in Partial"
+ end
+
+ def result!
+ raise "Invalid 'result!' in Partial"
+ end
+
+ def title(_)
+ raise "Invalid 'title' in Partial"
+ end
+
+ def instructions(_)
+ raise "Invalid 'instructions' in Partial"
+ end
+ end
end
end
M lib/legacy_customer.rb => lib/legacy_customer.rb +7 -14
@@ 17,7 17,7 @@ class LegacyCustomer
def info
EMPromise.resolve(nil).then do
- Info.new(jid: jid, tel: tel)
+ Info.new(tel: tel)
end
end
@@ 26,7 26,7 @@ class LegacyCustomer
info,
api
]).then do |info, api|
- AdminInfo.new(info: info, api: api)
+ AdminInfo.new(info: info, jid: jid, api: api)
end
end
@@ 36,30 36,23 @@ class LegacyCustomer
class Info
value_semantics do
- jid ProxiedJID, coerce: ProxiedJID.method(:new)
tel String
end
- def fields
- [
- { var: "JID", value: jid.unproxied.to_s },
- { var: "Phone Number", value: tel }
- ]
+ def form
+ FormTemplate.render("legacy_customer_info", info: self)
end
end
class AdminInfo
value_semantics do
info Info
+ jid ProxiedJID, coerce: ProxiedJID.method(:new)
api API
end
- def fields
- info.fields + [
- { var: "Account Status", value: "Legacy" },
- { var: "Cheo JID", value: info.jid.to_s },
- { var: "API", value: api.to_s }
- ]
+ def form
+ FormTemplate.render("legacy_customer_admin_info", admin_info: self)
end
end
end
M sgx_jmp.rb => sgx_jmp.rb +4 -11
@@ 434,10 434,7 @@ Command.new(
) {
Command.customer.then(&:info).then do |info|
Command.finish do |reply|
- form = Blather::Stanza::X.new(:result)
- form.title = "Account Info"
- form.fields = info.fields
- reply.command << form
+ reply.command << info.form
end
end
}.register(self).then(&CommandList.method(:register))
@@ 675,19 672,15 @@ Command.new(
Command.customer.then do |customer|
raise AuthError, "You are not an admin" unless customer&.admin?
- customer_info = CustomerInfoForm.new
Command.reply { |reply|
reply.allowed_actions = [:next]
- reply.command << customer_info.picker_form
+ reply.command << FormTemplate.render("customer_picker")
}.then { |response|
- customer_info.find_customer(response)
+ CustomerInfoForm.new.find_customer(response)
}.then do |target_customer|
target_customer.admin_info.then do |info|
Command.finish do |reply|
- form = Blather::Stanza::X.new(:result)
- form.title = "Customer Info"
- form.fields = info.fields
- reply.command << form
+ reply.command << info.form
end
end
end
M test/test_customer_info.rb => test/test_customer_info.rb +38 -20
@@ 5,6 5,14 @@ require "customer_info"
API::REDIS = FakeRedis.new
CustomerPlan::REDIS = Minitest::Mock.new
+PlanInfo::DB = FakeDB.new(
+ ["test"] => [
+ {
+ "start_date" => Time.parse("2020-01-01"),
+ "activation_date" => Time.parse("2021-01-01")
+ }
+ ]
+)
class CustomerInfoTest < Minitest::Test
def test_info_does_not_crash
@@ 18,8 26,15 @@ class CustomerInfoTest < Minitest::Test
["jmp_customer_auto_top_up_amount-test"]
)
- cust = customer(sgx: sgx)
- assert cust.info.sync.fields
+ CustomerPlan::DB.expect(
+ :query_defer,
+ EMPromise.resolve([{ "start_date" => Time.now }]),
+ [String, ["test"]]
+ )
+
+ cust = customer(sgx: sgx, plan_name: "test_usd")
+
+ assert cust.info.sync.form
assert_mock sgx
end
em :test_info_does_not_crash
@@ 28,6 43,8 @@ class CustomerInfoTest < Minitest::Test
sgx = Minitest::Mock.new
sgx.expect(:registered?, false)
sgx.expect(:registered?, false)
+ fwd = CustomerFwd.for(uri: "tel:+12223334444", timeout: 15)
+ sgx.expect(:fwd, fwd)
CustomerPlan::REDIS.expect(
:get,
@@ 35,8 52,14 @@ class CustomerInfoTest < Minitest::Test
["jmp_customer_auto_top_up_amount-test"]
)
- cust = customer(sgx: sgx)
- assert cust.admin_info.sync.fields
+ CustomerPlan::DB.expect(
+ :query_defer,
+ EMPromise.resolve([{ "start_date" => Time.now }]),
+ [String, ["test"]]
+ )
+
+ cust = customer(sgx: sgx, plan_name: "test_usd")
+ assert cust.admin_info.sync.form
assert_mock sgx
end
em :test_admin_info_does_not_crash
@@ 46,12 69,6 @@ class CustomerInfoTest < Minitest::Test
sgx.expect(:registered?, false)
sgx.expect(:registered?, false)
- CustomerPlan::REDIS.expect(
- :get,
- EMPromise.resolve(nil),
- ["jmp_customer_auto_top_up_amount-test"]
- )
-
plan = CustomerPlan.new("test", plan: nil, expires_at: nil)
cust = Customer.new(
"test",
@@ 59,7 76,7 @@ class CustomerInfoTest < Minitest::Test
plan: plan,
sgx: sgx
)
- assert cust.info.sync.fields
+ assert cust.info.sync.form
assert_mock sgx
end
em :test_inactive_info_does_not_crash
@@ 68,12 85,7 @@ class CustomerInfoTest < Minitest::Test
sgx = Minitest::Mock.new
sgx.expect(:registered?, false)
sgx.expect(:registered?, false)
-
- CustomerPlan::REDIS.expect(
- :get,
- EMPromise.resolve(nil),
- ["jmp_customer_auto_top_up_amount-test"]
- )
+ sgx.expect(:fwd, CustomerFwd::None.new(uri: nil, timeout: nil))
plan = CustomerPlan.new("test", plan: nil, expires_at: nil)
cust = Customer.new(
@@ 83,7 95,7 @@ class CustomerInfoTest < Minitest::Test
sgx: sgx
)
- assert cust.admin_info.sync.fields
+ assert cust.admin_info.sync.form
assert_mock sgx
end
em :test_inactive_admin_info_does_not_crash
@@ 93,7 105,7 @@ class CustomerInfoTest < Minitest::Test
Blather::JID.new("legacy@example.com"),
"+12223334444"
)
- assert cust.info.sync.fields
+ assert cust.info.sync.form
end
em :test_legacy_customer_info_does_not_crash
@@ 102,7 114,13 @@ class CustomerInfoTest < Minitest::Test
Blather::JID.new("legacy@example.com"),
"+12223334444"
)
- assert cust.admin_info.sync.fields
+ assert cust.admin_info.sync.form
end
em :test_legacy_customer_admin_info_does_not_crash
+
+ def test_missing_customer_admin_info_does_not_crash
+ cust = CustomerInfoForm::NoCustomer.new
+ assert cust.admin_info.form
+ end
+ em :test_missing_customer_admin_info_does_not_crash
end