Billing monthly cronjob using sgx-jmp

Stephen Paul Weber created

Just get the list of expired customers and tell sgx-jmp about each of them, wait
until all return or one errors and log result.

Change summary

bin/billing_monthly_cronjob | 255 +++++++-------------------------------
lib/blather_notify.rb       |   4 
2 files changed, 53 insertions(+), 206 deletions(-)

Detailed changes

bin/billing_monthly_cronjob 🔗

@@ -1,52 +1,26 @@
 #!/usr/bin/ruby
 # frozen_string_literal: true
 
-# Usage: ./billing_monthly_cronjob '{
-#        notify_using = {
-#          jid = "",
-#          password = "",
-#          target = \(jid: Text) -> "+12266669977@cheogram.com",
-#          body = \(jid: Text) -> \(body: Text) -> "/msg ${jid} ${body}",
-#        },
-#        plans = ./plans.dhall
-#        }'
-
-require "bigdecimal"
-require "date"
 require "dhall"
-require "net/http"
 require "pg"
-require "redis"
 
 require_relative "../lib/blather_notify"
 require_relative "../lib/to_form"
 
-using ToForm
-
 CONFIG = Dhall.load(<<-DHALL).sync
-	let Quota = < unlimited | limited: { included: Natural, price: Natural } >
-	let Currency = < CAD | USD >
-	in
 	(#{ARGV[0]}) : {
-		healthchecks_url: Text,
 		sgx_jmp: Text,
 		notify_using: {
 			jid: Text,
 			password: Text,
 			target: Text -> Text,
 			body: Text -> Text -> Text
-		},
-		plans: List {
-			name: Text,
-			currency: Currency,
-			monthly_price: Natural,
-			minutes: Quota,
-			messages: Quota
 		}
 	}
 DHALL
 
-REDIS = Redis.new
+using ToForm
+
 db = PG.connect(dbname: "jmp")
 db.type_map_for_results = PG::BasicTypeMapForResults.new(db)
 db.type_map_for_queries = PG::BasicTypeMapForQueries.new(db)
@@ -56,193 +30,62 @@ BlatherNotify.start(
 	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
+promises = []
+
+db.exec(
+	<<-SQL
+	SELECT customer_id
+	FROM customer_plans
+	WHERE expires_at <= LOCALTIMESTAMP + '4 days'
+	SQL
+).each do |row|
+	EM.next_tick do
+		promises << BlatherNotify.execute(
+			"customer info",
+			{ q: row["customer_id"] }.to_form(:submit)
+		).then { |iq|
+			BlatherNotify.write_with_promise(BlatherNotify.command(
+				"customer info",
+				iq.sessionid
+			))
+		}.then do |iq|
+			unless iq.form.field("action")
+				next "#{row["customer_id"]} not found"
+			end
 
-	def to_h
-		@stats.transform_values { |v| v.is_a?(BigDecimal) ? v.to_s("F") : v }
+			BlatherNotify.write_with_promise(BlatherNotify.command(
+				"customer info",
+				iq.sessionid,
+				action: :complete,
+				form: { action: "bill_plan" }.to_form(:submit)
+			))
+		end
 	end
 end
 
-stats = Stats.new(
-	not_renewed: 0,
-	renewed: 0,
-	not_registered: 0,
-	revenue: BigDecimal(0)
-)
-
-class Plan
-	def self.from_name(plan_name)
-		plan = CONFIG[:plans].find { |p| p[:name].to_s == plan_name }
-		new(plan) if plan
-	end
+one = Queue.new
 
-	def initialize(plan)
-		@plan = plan
-	end
-
-	def price
-		BigDecimal(@plan["monthly_price"].to_i) * 0.0001
-	end
-
-	def bill_customer(db, customer_id)
-		transaction_id = "#{customer_id}-renew-until-#{RENEW_UNTIL}"
-		db.exec_params(<<-SQL, [customer_id, transaction_id, -price])
-			INSERT INTO transactions
-				(customer_id, transaction_id, settled_after, amount, note)
-			VALUES
-				($1, $2, LOCALTIMESTAMP, $3, 'Renew account plan')
-		SQL
-	end
-
-	def renew(db, customer_id, expires_at)
-		bill_customer(db, customer_id)
-
-		params = [RENEW_UNTIL, customer_id, expires_at]
-		db.exec_params(<<-SQL, params)
-			UPDATE plan_log
-			SET date_range=range_merge(date_range, tsrange('now', $1))
-			WHERE customer_id=$2 AND date_range -|- tsrange($3, $3, '[]')
-		SQL
+def format(item)
+	if item.respond_to?(:note) && item.note
+		item.note.text
+	elsif item.respond_to?(:to_xml)
+		item.to_xml
+	else
+		item.inspect
 	end
 end
 
-class ExpiredCustomer
-	def self.for(row, db)
-		plan = Plan.from_name(row["plan_name"])
-		if row["balance"] < plan.price
-			WithLowBalance.new(row, plan, db)
-		else
-			new(row, plan, db)
-		end
-	end
-
-	def initialize(row, plan, db)
-		@row = row
-		@plan = plan
-		@db = db
-	end
-
-	def customer_id
-		@row["customer_id"]
-	end
-
-	def try_renew(db, stats)
-		@plan.renew(
-			db,
-			customer_id,
-			@row["expires_at"]
-		)
-
-		stats.add(:renewed, 1)
-		stats.add(:revenue, @plan.price)
-	end
-
-	class WithLowBalance < ExpiredCustomer
-		ONE_WEEK = 60 * 60 * 24 * 7
-		LAST_WEEK = Time.now - ONE_WEEK
-
-		def try_renew(_, stats)
-			stats.add(:not_renewed, 1)
-			topup = "jmp_customer_auto_top_up_amount-#{customer_id}"
-			if REDIS.exists?(topup) && @row["expires_at"] > LAST_WEEK
-				@db.exec_params(
-					"SELECT pg_notify('low_balance', $1)",
-					[customer_id]
-				)
-			else
-				notify_if_needed
-			end
-		end
-
-	protected
-
-		def notify_if_needed
-			return if REDIS.exists?("jmp_customer_low_balance-#{customer_id}")
-
-			REDIS.set(
-				"jmp_customer_low_balance-#{customer_id}",
-				Time.now, ex: ONE_WEEK
-			)
-			send_notification
-		end
-
-		def jid
-			REDIS.get("jmp_customer_jid-#{customer_id}")
-		end
-
-		def tel
-			REDIS.lindex("catapult_cred-customer_#{customer_id}@jmp.chat", 3)
-		end
-
-		def btc_addresses
-			@btc_addresses ||= REDIS.smembers(
-				"jmp_customer_btc_addresses-#{customer_id}"
-			)
-		end
-
-		def btc_addresses_for_notification
-			return if btc_addresses.empty?
-
-			"\nYou can buy credit by sending any amount of Bitcoin to one of "\
-			"these addresses:\n#{btc_addresses.join("\n")}"
-		end
-
-		def send_notification
-			raise "No JID for #{customer_id}, cannot notify" unless jid
-
-			BlatherNotify.say(
-				CONFIG[:notify_using][:target].call(jid),
-				CONFIG[:notify_using][:body].call(
-					jid, renewal_notification
-				)
-			)
-		end
-
-		def renewal_notification
-			"Failed to renew account for #{tel}, " \
-			"balance of $#{'%.4f' % @row['balance']} is too low. " \
-			"To keep your number, please buy more credit soon. " \
-			"#{btc_addresses_for_notification}"
-		end
-	end
+EM.add_timer(0) do
+	EMPromise.all(promises).then(
+		->(all) { one << all },
+		->(err) { one << RuntimeError.new(format(err)) }
+	)
 end
 
-db.transaction do
-	db.exec(
-		<<-SQL
-		SELECT customer_id, plan_name, expires_at, COALESCE(balance, 0) AS balance
-		FROM customer_plans LEFT JOIN balances USING (customer_id)
-		WHERE expires_at <= NOW()
-		SQL
-	).each do |row|
-		one = Queue.new
-		EM.next_tick do
-			BlatherNotify.execute(
-				"customer info",
-				{ q: row["customer_id"] }.to_form(:submit)
-			).then(
-				->(x) { one << x },
-				->(e) { one << RuntimeError.new(e.to_s) }
-			)
-		end
-		info = one.pop
-		raise info if info.is_a?(Exception)
+result = one.pop
 
-		if info.form.field("tel")&.value
-			ExpiredCustomer.for(row, db).try_renew(db, stats)
-		else
-			stats.add(:not_registered, 1)
-		end
-	end
-end
+raise result if result.is_a?(Exception)
 
-p stats
+result.each do |item|
+	puts format(item)
+end

lib/blather_notify.rb 🔗

@@ -42,6 +42,10 @@ module BlatherNotify
 
 	def self.write_with_promise(stanza)
 		promise = EMPromise.new
+		EM.add_timer(15) do
+			promise.reject(:timeout)
+		end
+
 		client.write_with_handler(stanza) do |s|
 			if s.error?
 				promise.reject(s)