~singpolyma/jmp-pay

d33d2a2de6b09434c81ba7fe7c1af40c9b96e950 — Stephen Paul Weber a month ago fdedfcb + 5f5ccfc
Merge branch 'cancel-expired'

* cancel-expired:
  Safety guard
  Add script to cancel expired customers
2 files changed, 166 insertions(+), 1 deletions(-)

M Gemfile
A bin/cancel_expired_customers
M Gemfile => Gemfile +3 -1
@@ 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"


A bin/cancel_expired_customers => bin/cancel_expired_customers +163 -0
@@ 0,0 1,163 @@
#!/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
			raise "ported_in looks wrong" if tns.length < 250

			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)