From 456a45b1c3fb8f91d6520d5fdd922c9cf9f2f56b Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 28 Mar 2022 16:07:01 -0500 Subject: [PATCH 1/4] Clearer name for lock bypass factory --- lib/low_balance.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/low_balance.rb b/lib/low_balance.rb index e5233db05f039ee206e568186b059661407cdb1a..41bbdd8639cc50fa894cde4811ea7882c1163b48 100644 --- a/lib/low_balance.rb +++ b/lib/low_balance.rb @@ -11,11 +11,11 @@ class LowBalance "jmp_customer_low_balance-#{customer.customer_id}", expiry: 60 * 60 * 24 * 7 ).with(-> { Locked.new }) do - for_auto_top_up_amount(customer) + for_no_lock(customer) end end - def self.for_auto_top_up_amount(customer) + def self.for_no_lock(customer) if customer.auto_top_up_amount.positive? AutoTopUp.new(customer) else From 655624ddc816cffeeb47ec50a0715bc6a24e5ff0 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 28 Mar 2022 16:07:27 -0500 Subject: [PATCH 2/4] Bill plan command Implements the full logic of the billing_monthly_cronjob for one customer by reusing code that has been written more robustly in sgx-jmp for some time. This will allow the cronjob to just execute this command once a day for each expiring customer to get all the correct billing behaviours. --- forms/admin_menu.rb | 3 +- lib/admin_command.rb | 5 +++ lib/bill_plan_command.rb | 69 ++++++++++++++++++++++++++++++++++++++++ lib/customer.rb | 3 +- 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 lib/bill_plan_command.rb diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index 486f3eb3524ccba3e70a4058f6ed28d70d5f06d1..4d5fa498f5cf0d63dd876d4f87342304e6eba39b 100644 --- a/forms/admin_menu.rb +++ b/forms/admin_menu.rb @@ -9,6 +9,7 @@ field( description: "or put a new customer info", options: [ { value: "info", label: "Customer Info" }, - { value: "financial", label: "Customer Billing Information" } + { value: "financial", label: "Customer Billing Information" }, + { value: "bill_plan", label: "Bill Customer" } ] ) diff --git a/lib/admin_command.rb b/lib/admin_command.rb index f0994d46114cc779c516430e0e25d76a7db0f359..0b8e02821a574adca161b5cb68b044ee38e792b8 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "bill_plan_command" require_relative "customer_info_form" require_relative "financial_info" require_relative "form_template" @@ -65,6 +66,10 @@ class AdminCommand end end + def action_bill_plan + BillPlanCommand.for(@target_customer).call + end + def pay_methods(financial_info) reply(FormTemplate.render( "admin_payment_methods", diff --git a/lib/bill_plan_command.rb b/lib/bill_plan_command.rb new file mode 100644 index 0000000000000000000000000000000000000000..81ede5fe905522f9dd2f714080d3275486e5d0d8 --- /dev/null +++ b/lib/bill_plan_command.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class BillPlanCommand + def self.for(customer) + return ForUnregistered.new unless customer.registered? + + unless customer.balance > customer.monthly_price + return ForLowBalance.new(customer) + end + + new(customer) + end + + def initialize(customer) + @customer = customer + end + + def call + @customer.bill_plan + Command.reply do |reply| + reply.note_type = :info + reply.note_text = "Customer billed" + end + end + + class ForLowBalance + def initialize(customer) + @customer = customer + end + + def call + LowBalance.for(@customer).then(&:notify!).then do |amount| + return command_for(amount).call if amount&.positive? + + notify_failure + Command.reply do |reply| + reply.note_type = :error + reply.note_text = "Customer balance is too low" + end + end + end + + protected + + def notify_failure + m = Blather::Stanza::Message.new + m.from = CONFIG[:notify_from] + m.body = + "Failed to renew account for #{@customer.registered?.phone}. " \ + "To keep your number, please buy more credit soon." + @customer.stanza_to(m) + end + + def command_for(amount) + BillPlanCommand.for( + @customer.with_balance(@customer.balance + amount) + ) + end + end + + class ForUnregistered + def call + Command.reply do |reply| + reply.note_type = :error + reply.note_text = "Customer is not registered" + end + end + end +end diff --git a/lib/customer.rb b/lib/customer.rb index 5cf426a180217952b97f9c1fd8456c1cf9b1d9bb..af54dd0ce16ad9a1f084727dbadb99a4dec70e8b 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -24,7 +24,8 @@ class Customer def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan, :currency, :merchant_account, :plan_name, :minute_limit, - :message_limit, :auto_top_up_amount, :monthly_overage_limit + :message_limit, :auto_top_up_amount, :monthly_overage_limit, + :monthly_price def_delegators :@sgx, :register!, :registered?, :set_ogm_url, :fwd, :transcription_enabled def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage From 8a4e12980a23f7752f11f4fc8e511411ffd7c839 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 30 Mar 2022 11:21:16 -0500 Subject: [PATCH 3/4] Command.execution setter --- lib/command.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/command.rb b/lib/command.rb index 4292d22bf62efc879a74e1f747523866cc7d8ac4..da8116293a5d5224716ca896190c790bb6a71df1 100644 --- a/lib/command.rb +++ b/lib/command.rb @@ -11,6 +11,10 @@ class Command Thread.current[:execution] end + def self.execution=(exe) + Thread.current[:execution] = exe + end + def self.reply(stanza=nil, &blk) execution.reply(stanza, &blk) end @@ -51,7 +55,7 @@ class Command def execute StatsD.increment("command", tags: ["node:#{iq.node}"]) EMPromise.resolve(nil).then { - Thread.current[:execution] = self + Command.execution = self sentry_hub catch_after(EMPromise.resolve(yield self)) }.catch(&method(:panic)) From 3b7abebd4df7b8508edd310704b098095c3e17a7 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 29 Mar 2022 15:45:50 -0500 Subject: [PATCH 4/4] Allow the DB to notify us to bill a customer --- lib/db_notification.rb | 17 +++++++++++++++++ lib/dummy_command.rb | 17 +++++++++++++++++ sgx_jmp.rb | 28 +++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 lib/db_notification.rb create mode 100644 lib/dummy_command.rb diff --git a/lib/db_notification.rb b/lib/db_notification.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9b7f1f7545976920904568b8eea1c1db5c767c9 --- /dev/null +++ b/lib/db_notification.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "dummy_command" + +module DbNotification + def self.for(notify, customer) + case notify[:relname] + when "low_balance" + LowBalance.for(customer).then { |lb| lb.method(:notify!) } + when "possible_renewal" + Command.execution = DummyCommand.new(customer) + BillPlanCommand.for(customer) + else + raise "Unknown notification: #{notify[:relname]}" + end + end +end diff --git a/lib/dummy_command.rb b/lib/dummy_command.rb new file mode 100644 index 0000000000000000000000000000000000000000..7df14ceb5c3adff3109fb0cfa41547fb9f0a54b1 --- /dev/null +++ b/lib/dummy_command.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class DummyCommand + attr_reader :customer + + def initialize(customer) + @customer = customer + end + + def reply(*); end + + def finish(*); end + + def log + ::LOG + end +end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 7891cd95cce055283b63d25fb43ed68f9aeb3f35..cc2b0458ca7ab511003b779adec11616567bdf71 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -81,6 +81,8 @@ require_relative "lib/command_list" require_relative "lib/customer" require_relative "lib/customer_info_form" require_relative "lib/customer_repo" +require_relative "lib/dummy_command" +require_relative "lib/db_notification" require_relative "lib/electrum" require_relative "lib/empty_repo" require_relative "lib/expiring_lock" @@ -178,10 +180,27 @@ end EM.error_handler(&method(:panic)) +# Infer anything we might have been notified about while we were down +def catchup_notify(db) + db.query("SELECT customer_id FROM balances WHERE balance < 5").each do |c| + db.query("SELECT pg_notify('low_balance', $1)", c.values) + end + db.query(<<~SQL).each do |c| + SELECT customer_id + FROM customer_plans INNER JOIN balances USING (customer_id) + WHERE expires_at < LOCALTIMESTAMP AND balance >= 5 + SQL + db.query("SELECT pg_notify('possible_renewal', $1)", c.values) + end +end + def poll_for_notify(db) db.wait_for_notify_defer.then { |notify| - CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new).find(notify[:extra]) - }.then(&LowBalance.method(:for)).then(&:notify!).then { + CustomerRepo + .new(sgx_repo: Bwmsgsv2Repo.new) + .find(notify[:extra]) + .then { |customer| DbNotification.for(notify, customer) } + }.then(&:call).then { poll_for_notify(db) }.catch(&method(:panic)) end @@ -208,9 +227,8 @@ when_ready do DB.hold do |conn| conn.query("LISTEN low_balance") - conn.query("SELECT customer_id FROM balances WHERE balance < 5").each do |c| - conn.query("SELECT pg_notify('low_balance', $1)", c.values) - end + conn.query("LISTEN possible_renewal") + catchup_notify(conn) poll_for_notify(conn) end