@@ 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"
@@ 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)