Add script to cancel expired customers

Stephen Paul Weber created

Change summary

Gemfile                      |   4 
bin/cancel_expired_customers | 161 ++++++++++++++++++++++++++++++++++++++
2 files changed, 164 insertions(+), 1 deletion(-)

Detailed changes

Gemfile 🔗

@@ -5,12 +5,14 @@ source "https://rubygems.org"
 gem "blather"
 gem "braintree"
 gem "dhall", ">= 0.5.3.fixed"
+gem "em-http-request"
 gem "em_promise.rb"
+gem "em-synchrony"
 gem "money-open-exchange-rates"
 gem "pg"
 gem "redis"
 gem "roda"
-gem "ruby-bandwidth-iris"
+gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "list-port-ins"
 gem "sentry-ruby"
 gem "slim"
 

bin/cancel_expired_customers 🔗

@@ -0,0 +1,161 @@
+#!/usr/bin/ruby
+# frozen_string_literal: true
+
+require "date"
+require "dhall"
+require "em_promise"
+require "pg"
+require "ruby-bandwidth-iris"
+require "set"
+
+require_relative "../lib/blather_notify"
+require_relative "../lib/to_form"
+
+CONFIG = Dhall.load(<<-DHALL).sync
+	(#{ARGV[0]}) : {
+		sgx_jmp: Text,
+		creds: {
+			account: Text,
+			username: Text,
+			password: Text
+		},
+		notify_using: {
+			jid: Text,
+			password: Text,
+			target: Text -> Text,
+			body: Text -> Text -> Text
+		}
+	}
+DHALL
+
+Faraday.default_adapter = :em_synchrony
+BandwidthIris::Client.global_options = {
+	account_id: CONFIG[:creds][:account],
+	username: CONFIG[:creds][:username],
+	password: CONFIG[:creds][:password]
+}
+
+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)
+
+BlatherNotify.start(
+	CONFIG[:notify_using][:jid],
+	CONFIG[:notify_using][:password]
+)
+
+def format(item)
+	if item.respond_to?(:note) && item.note && item.note.text != ""
+		item.note.text
+	elsif item.respond_to?(:to_xml)
+		item.to_xml
+	else
+		item.inspect
+	end
+end
+
+ported_in_promise = Promise.new
+
+EM.schedule do
+	Fiber.new {
+		begin
+			tns = Set.new
+			page = BandwidthIris::PortIn.list(
+				page: 1,
+				size: 1000,
+				status: :complete
+			)
+			while page
+				page.each_slice(250) do |orders|
+					EMPromise.all(
+						orders.map { |order|
+							EMPromise.resolve(nil).then { order.tns }
+						}
+					).sync.each { |chunk| tns += chunk.map { |tn| "+1#{tn}" } }
+				end
+				page = page.next
+			end
+			ported_in_promise.fulfill(tns)
+		rescue StandardError
+			ported_in_promise.reject($!)
+		end
+	}.resume
+end
+
+class ExpiringCustomer
+	def initialize(customer_id)
+		@customer_id = customer_id
+	end
+
+	def info
+		BlatherNotify.execute(
+			"customer info",
+			{ q: @customer_id }.to_form(:submit)
+		).then do |iq|
+			@sessionid = iq.sessionid
+			unless iq.form.field("customer_id")
+				raise "#{@customer_id} not found"
+			end
+
+			iq
+		end
+	end
+
+	def next
+		raise "Call info first" unless @sessionid
+
+		BlatherNotify.write_with_promise(BlatherNotify.command(
+			"customer info",
+			@sessionid
+		))
+	end
+
+	def cancel_account
+		raise "Call info first" unless @sessionid
+
+		BlatherNotify.write_with_promise(BlatherNotify.command(
+			"customer info",
+			@sessionid,
+			action: :complete,
+			form: { action: "cancel_account" }.to_form(:submit)
+		))
+	end
+end
+
+one = Queue.new
+
+ported_in_promise.then { |ported_in|
+	EM::Iterator.new(db.exec(
+		<<-SQL
+		SELECT customer_id, expires_at FROM customer_plans
+		WHERE expires_at < LOCALTIMESTAMP - INTERVAL '1 month'
+		SQL
+	), 3).each(nil, -> { one << :done }) do |row, iter|
+		customer = ExpiringCustomer.new(row["customer_id"])
+		customer.info.then { |iq|
+			if ported_in.include?(iq.form.field("tel")&.value&.to_s) &&
+			   row["expires_at"] > (Date.today << 12)
+				puts "#{row['customer_id']} ported in, skipping"
+				EMPromise.reject(:skip)
+			else
+				customer.next
+			end
+		}.then {
+			customer.cancel_account
+		}.then { |result|
+			puts format(result)
+			iter.next
+		}.catch do |err|
+			next iter.next if err == :skip
+
+			one << (err.is_a?(Exception) ? err : RuntimeError.new(format(err)))
+		end
+	end
+}.catch do |err|
+	one << (err.is_a?(Exception) ? err : RuntimeError.new(format(err)))
+end
+
+result = one.pop
+raise result if result.is_a?(Exception)