#!/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 }, old_port_ins: List 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(CONFIG[:old_port_ins]) 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 @info = iq end end def next raise "Call info first" unless @sessionid && @info return EMPromise.reject(:skip) unless @info.form.field("tel") 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 module SnikketInstanceManager def self.stop_instances(instance_ids) EMPromise.all(instance_ids.map { |id| stop_instance(id) }) end def self.stop_instance(instance_id) BlatherNotify.execute( "stop snikket", { instance_id: instance_id }.to_form(:submit) ) end end one = Queue.new ported_in_promise.then { |ported_in| EM::Iterator.new(db.exec( <<-SQL SELECT customer_plans.customer_id, customer_plans.expires_at, array_agg(snikket_instances.instance_id) as instance_ids FROM customer_plans LEFT JOIN snikket_instances ON customer_plans.customer_id = snikket_instances.customer_id WHERE customer_plans.expires_at < LOCALTIMESTAMP - INTERVAL '1 month' GROUP BY customer_plans.customer_id, customer_plans.expires_at 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).to_time puts "#{row['customer_id']} ported in, skipping" EMPromise.reject(:skip) else customer.next end }.then { customer.cancel_account }.then { |result| puts format(result) SnikketInstanceManager.stop_instances(row["instance_ids"]) }.then { |stopped_instances| puts "Stopped #{stopped_instances.length} Snikket instance(s)" 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)