From 7c438f83822c6a2f41da32b93bbaa48203f0932b Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 2 Mar 2021 12:39:52 -0500 Subject: [PATCH] Notify customer when renewal fails due to low balance This is done by sending from a configured JID to @cheogram.com in order to have them receive a message from support. --- Gemfile | 1 + bin/billing_monthly_cronjob | 136 ++++++++++++++++++++++++++++-------- lib/blather_notify.rb | 28 ++++++++ 3 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 lib/blather_notify.rb diff --git a/Gemfile b/Gemfile index 5bf4014..0f14ebe 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "blather" gem "braintree" gem "dhall" gem "money-open-exchange-rates" diff --git a/bin/billing_monthly_cronjob b/bin/billing_monthly_cronjob index 3cb91af..129644b 100755 --- a/bin/billing_monthly_cronjob +++ b/bin/billing_monthly_cronjob @@ -1,29 +1,76 @@ #!/usr/bin/ruby # frozen_string_literal: true -# Usage: ./billing_monthly_cronjob \ -# '{ healthchecks_url = "https://hc-ping.com/...", plans = ./plans.dhall }' +# Usage: ./billing_monthly_cronjob '{ +# healthchecks_url = "https://hc-ping.com/...", +# notify_using = { +# jid = "", +# password = "", +# target = \(tel: Text) -> "${tel}@cheogram.com" +# }, +# plans = ./plans.dhall +# }' require "bigdecimal" require "date" require "dhall" require "net/http" require "pg" - -CONFIG = Dhall.load(ARGV[0]).sync +require "redis" + +require_relative "../lib/blather_notify" + +CONFIG = Dhall.load(<<-DHALL).sync + let Quota = < unlimited | limited: { included: Natural, price: Natural } > + let Currency = < CAD | USD > + in + (#{ARGV[0]}) : { + healthchecks_url: Text, + notify_using: { jid: Text, password: Text, target: Text -> Text }, + plans: List { + name: Text, + currency: Currency, + monthly_price: Natural, + minutes: Quota, + messages: Quota + } + } +DHALL Net::HTTP.post_form(URI("#{CONFIG[:healthchecks_url]}/start"), {}) +REDIS = Redis.new db = PG.connect(dbname: "jmp") db.type_map_for_results = PG::BasicTypeMapForResults.new(db) db.type_map_for_queries = PG::BasicTypeMapForQueries.new(db) -not_renewed = 0 -renewed = 0 -revenue = BigDecimal.new(0) +BlatherNotify.start( + CONFIG[:notify_using][:jid], + CONFIG[:notify_using][:password] +) RENEW_UNTIL = Date.today >> 1 +class Stats + def initialize(**kwargs) + @stats = kwargs + end + + def add(stat, value) + @stats[stat] += value + end + + def to_h + @stats.transform_values { |v| v.is_a?(BigDecimal) ? v.to_s("F") : v } + end +end + +stats = Stats.new( + not_renewed: 0, + renewed: 0, + revenue: BigDecimal.new(0) +) + class Plan def self.from_name(plan_name) plan = CONFIG[:plans].find { |p| p[:name].to_s == plan_name } @@ -59,6 +106,56 @@ class Plan end end +class ExpiredCustomer + def self.for(row) + plan = Plan.from_name(row["plan_name"]) + if row["balance"] < plan.price + WithLowBalance.new(row, plan) + else + new(row, plan) + end + end + + def initialize(row, plan) + @row = row + @plan = plan + end + + def try_renew(db, stats) + @plan.renew( + db, + @row["customer_id"], + @row["expires_at"] + ) + + stats.add(:renewed, 1) + stats.add(:revenue, plan.price) + end + + class WithLowBalance < ExpiredCustomer + def try_renew(_, stats) + jid = REDIS.get("jmp_customer_jid-#{@row['customer_id']}") + tel = REDIS.lindex("catapult_cred-#{jid}", 3) + BlatherNotify.say( + CONFIG[:notify_using][:target].call(tel.to_s), + format_renewal_notification(tel) + ) + + stats.add(:not_renewed, 1) + end + + protected + + def format_renewal_notification(tel) + <<~NOTIFY + Failed to renew account for #{tel}, + balance of #{@row['balance']} is too low. + To keep your number, please buy more credit soon. + NOTIFY + end + end +end + db.transaction do db.exec( <<-SQL @@ -66,28 +163,9 @@ db.transaction do FROM customer_plans INNER JOIN balances USING (customer_id) WHERE expires_at <= NOW() SQL - ).each do |expired_customer| - plan = Plan.from_name(expired_customer["plan_name"]) - - if expired_customer["balance"] < plan.price - not_renewed += 1 - next - end - - plan.renew( - db, - expired_customer["customer_id"], - expired_customer["expires_at"] - ) - - renewed += 1 - revenue += plan.price + ).each do |row| + ExpiredCustomer.for(row).try_renew(db, stats) end end -Net::HTTP.post_form( - URI(CONFIG[:healthchecks_url].to_s), - renewed: renewed, - not_renewed: not_renewed, - revenue: revenue.to_s("F") -) +Net::HTTP.post_form(URI(CONFIG[:healthchecks_url].to_s), **stats.to_h) diff --git a/lib/blather_notify.rb b/lib/blather_notify.rb new file mode 100644 index 0000000..4152aa0 --- /dev/null +++ b/lib/blather_notify.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "blather/client/dsl" +require "timeout" + +module BlatherNotify + extend Blather::DSL + + @ready = Queue.new + + when_ready { @ready << :ready } + + def self.start(jid, password) + # workqueue_count MUST be 0 or else Blather uses threads! + setup(jid, password, nil, nil, nil, nil, workqueue_count: 0) + + EM.error_handler { |e| warn e.message } + Thread.new do + EM.run do + client.run + end + end + + at_exit { shutdown } + + Timeout.timeout(5) { @ready.pop } + end +end -- 2.38.5