From c7c48663714307e3598dc14848e539fb8672b0b7 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Wed, 5 Jan 2022 16:23:33 -0500 Subject: [PATCH] Customer Info Forms and More Info There's a bit extra info I wanted about users, so since I was doing this anyway I figured I may as well port the existing forms to the new form renderer. Then, in order to fit within the guidelines I needed subforms, so partials were added. --- Gemfile | 1 + forms/admin_info.rb | 53 ++++++++++ forms/admin_plan_info.rb | 6 ++ forms/customer_info.rb | 4 + forms/customer_info_partial.rb | 29 ++++++ forms/customer_picker.rb | 14 +++ forms/legacy_customer_admin_info.rb | 29 ++++++ forms/legacy_customer_info.rb | 4 + forms/legacy_customer_info_partial.rb | 11 ++ forms/no_customer_info.rb | 8 ++ forms/plan_info.rb | 38 +++++++ lib/customer.rb | 4 +- lib/customer_fwd.rb | 4 + lib/customer_info.rb | 140 +++++++++++++++----------- lib/customer_info_form.rb | 8 +- lib/customer_plan.rb | 12 +++ lib/form_template.rb | 72 ++++++++++++- lib/legacy_customer.rb | 21 ++-- sgx_jmp.rb | 15 +-- test/test_customer_info.rb | 58 +++++++---- 20 files changed, 414 insertions(+), 117 deletions(-) create mode 100644 forms/admin_info.rb create mode 100644 forms/admin_plan_info.rb create mode 100644 forms/customer_info.rb create mode 100644 forms/customer_info_partial.rb create mode 100644 forms/customer_picker.rb create mode 100644 forms/legacy_customer_admin_info.rb create mode 100644 forms/legacy_customer_info.rb create mode 100644 forms/legacy_customer_info_partial.rb create mode 100644 forms/no_customer_info.rb create mode 100644 forms/plan_info.rb diff --git a/Gemfile b/Gemfile index 9b935ed..346235c 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/forms/admin_info.rb b/forms/admin_info.rb new file mode 100644 index 0000000..bff2ca1 --- /dev/null +++ b/forms/admin_info.rb @@ -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 diff --git a/forms/admin_plan_info.rb b/forms/admin_plan_info.rb new file mode 100644 index 0000000..21c2a3e --- /dev/null +++ b/forms/admin_plan_info.rb @@ -0,0 +1,6 @@ +field( + var: "start_date", + label: "Customer since", + description: @plan_info.relative_start_date, + value: @plan_info.formatted_start_date +) diff --git a/forms/customer_info.rb b/forms/customer_info.rb new file mode 100644 index 0000000..2522278 --- /dev/null +++ b/forms/customer_info.rb @@ -0,0 +1,4 @@ +result! +title "Account Info" + +render "customer_info_partial" diff --git a/forms/customer_info_partial.rb b/forms/customer_info_partial.rb new file mode 100644 index 0000000..51d51b4 --- /dev/null +++ b/forms/customer_info_partial.rb @@ -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 diff --git a/forms/customer_picker.rb b/forms/customer_picker.rb new file mode 100644 index 0000000..46f3c42 --- /dev/null +++ b/forms/customer_picker.rb @@ -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" +) diff --git a/forms/legacy_customer_admin_info.rb b/forms/legacy_customer_admin_info.rb new file mode 100644 index 0000000..fdf3a64 --- /dev/null +++ b/forms/legacy_customer_admin_info.rb @@ -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]}" +) diff --git a/forms/legacy_customer_info.rb b/forms/legacy_customer_info.rb new file mode 100644 index 0000000..7e78941 --- /dev/null +++ b/forms/legacy_customer_info.rb @@ -0,0 +1,4 @@ +result! +title "Account Info" + +render "legacy_customer_info_partial" diff --git a/forms/legacy_customer_info_partial.rb b/forms/legacy_customer_info_partial.rb new file mode 100644 index 0000000..46c8b7e --- /dev/null +++ b/forms/legacy_customer_info_partial.rb @@ -0,0 +1,11 @@ +field( + var: "account_status", + label: "Account Status", + value: "Legacy" +) + +field( + var: "tel", + label: "Phone Number", + value: @info.tel +) diff --git a/forms/no_customer_info.rb b/forms/no_customer_info.rb new file mode 100644 index 0000000..875970e --- /dev/null +++ b/forms/no_customer_info.rb @@ -0,0 +1,8 @@ +result! +title "Customer Info" + +field( + var: "account_status", + label: "Account Status", + value: "Not Found" +) diff --git a/forms/plan_info.rb b/forms/plan_info.rb new file mode 100644 index 0000000..f5c1e18 --- /dev/null +++ b/forms/plan_info.rb @@ -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 diff --git a/lib/customer.rb b/lib/customer.rb index f580bc1..c948213 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -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 diff --git a/lib/customer_fwd.rb b/lib/customer_fwd.rb index 400bee3..b723097 100644 --- a/lib/customer_fwd.rb +++ b/lib/customer_fwd.rb @@ -32,6 +32,10 @@ class CustomerFwd def to_i @timeout end + + def to_s + to_i.to_s + end end value_semantics do diff --git a/lib/customer_info.rb b/lib/customer_info.rb index 6d2bb5a..80f9d05 100644 --- a/lib/customer_info.rb +++ b/lib/customer_info.rb @@ -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 diff --git a/lib/customer_info_form.rb b/lib/customer_info_form.rb index 1ab7868..597ecec 100644 --- a/lib/customer_info_form.rb +++ b/lib/customer_info_form.rb @@ -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 diff --git a/lib/customer_plan.rb b/lib/customer_plan.rb index 00967ff..5aa3569 100644 --- a/lib/customer_plan.rb +++ b/lib/customer_plan.rb @@ -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 diff --git a/lib/form_template.rb b/lib/form_template.rb index ea0e371..b04bd64 100644 --- a/lib/form_template.rb +++ b/lib/form_template.rb @@ -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 diff --git a/lib/legacy_customer.rb b/lib/legacy_customer.rb index c67e329..3767d6c 100644 --- a/lib/legacy_customer.rb +++ b/lib/legacy_customer.rb @@ -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 diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 67e9a09..3129d07 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -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 diff --git a/test/test_customer_info.rb b/test/test_customer_info.rb index c383dff..abf5776 100644 --- a/test/test_customer_info.rb +++ b/test/test_customer_info.rb @@ -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 -- 2.38.5