@@ 1,29 1,76 @@
#!/usr/bin/ruby
# frozen_string_literal: true
-# Usage: ./billing_monthly_cronjob \
-# '{ healthchecks_url = "https://hc-ping.com/...", plans = ./plans.dhall }'
+# Usage: ./billing_monthly_cronjob '{
+# healthchecks_url = "https://hc-ping.com/...",
+# notify_using = {
+# jid = "",
+# password = "",
+# target = \(tel: Text) -> "${tel}@cheogram.com"
+# },
+# plans = ./plans.dhall
+# }'
require "bigdecimal"
require "date"
require "dhall"
require "net/http"
require "pg"
-
-CONFIG = Dhall.load(ARGV[0]).sync
+require "redis"
+
+require_relative "../lib/blather_notify"
+
+CONFIG = Dhall.load(<<-DHALL).sync
+ let Quota = < unlimited | limited: { included: Natural, price: Natural } >
+ let Currency = < CAD | USD >
+ in
+ (#{ARGV[0]}) : {
+ healthchecks_url: Text,
+ notify_using: { jid: Text, password: Text, target: Text -> Text },
+ plans: List {
+ name: Text,
+ currency: Currency,
+ monthly_price: Natural,
+ minutes: Quota,
+ messages: Quota
+ }
+ }
+DHALL
Net::HTTP.post_form(URI("#{CONFIG[:healthchecks_url]}/start"), {})
+REDIS = Redis.new
db = PG.connect(dbname: "jmp")
db.type_map_for_results = PG::BasicTypeMapForResults.new(db)
db.type_map_for_queries = PG::BasicTypeMapForQueries.new(db)
-not_renewed = 0
-renewed = 0
-revenue = BigDecimal.new(0)
+BlatherNotify.start(
+ CONFIG[:notify_using][:jid],
+ 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
+
+ def to_h
+ @stats.transform_values { |v| v.is_a?(BigDecimal) ? v.to_s("F") : v }
+ end
+end
+
+stats = Stats.new(
+ not_renewed: 0,
+ renewed: 0,
+ revenue: BigDecimal.new(0)
+)
+
class Plan
def self.from_name(plan_name)
plan = CONFIG[:plans].find { |p| p[:name].to_s == plan_name }
@@ 59,6 106,56 @@ class Plan
end
end
+class ExpiredCustomer
+ def self.for(row)
+ plan = Plan.from_name(row["plan_name"])
+ if row["balance"] < plan.price
+ WithLowBalance.new(row, plan)
+ else
+ new(row, plan)
+ end
+ end
+
+ def initialize(row, plan)
+ @row = row
+ @plan = plan
+ end
+
+ def try_renew(db, stats)
+ @plan.renew(
+ db,
+ @row["customer_id"],
+ @row["expires_at"]
+ )
+
+ stats.add(:renewed, 1)
+ stats.add(:revenue, plan.price)
+ end
+
+ class WithLowBalance < ExpiredCustomer
+ def try_renew(_, stats)
+ jid = REDIS.get("jmp_customer_jid-#{@row['customer_id']}")
+ tel = REDIS.lindex("catapult_cred-#{jid}", 3)
+ BlatherNotify.say(
+ CONFIG[:notify_using][:target].call(tel.to_s),
+ format_renewal_notification(tel)
+ )
+
+ stats.add(:not_renewed, 1)
+ end
+
+ protected
+
+ def format_renewal_notification(tel)
+ <<~NOTIFY
+ Failed to renew account for #{tel},
+ balance of #{@row['balance']} is too low.
+ To keep your number, please buy more credit soon.
+ NOTIFY
+ end
+ end
+end
+
db.transaction do
db.exec(
<<-SQL
@@ 66,28 163,9 @@ db.transaction do
FROM customer_plans INNER JOIN balances USING (customer_id)
WHERE expires_at <= NOW()
SQL
- ).each do |expired_customer|
- plan = Plan.from_name(expired_customer["plan_name"])
-
- if expired_customer["balance"] < plan.price
- not_renewed += 1
- next
- end
-
- plan.renew(
- db,
- expired_customer["customer_id"],
- expired_customer["expires_at"]
- )
-
- renewed += 1
- revenue += plan.price
+ ).each do |row|
+ ExpiredCustomer.for(row).try_renew(db, stats)
end
end
-Net::HTTP.post_form(
- URI(CONFIG[:healthchecks_url].to_s),
- renewed: renewed,
- not_renewed: not_renewed,
- revenue: revenue.to_s("F")
-)
+Net::HTTP.post_form(URI(CONFIG[:healthchecks_url].to_s), **stats.to_h)
@@ 0,0 1,28 @@
+# frozen_string_literal: true
+
+require "blather/client/dsl"
+require "timeout"
+
+module BlatherNotify
+ extend Blather::DSL
+
+ @ready = Queue.new
+
+ when_ready { @ready << :ready }
+
+ def self.start(jid, password)
+ # workqueue_count MUST be 0 or else Blather uses threads!
+ setup(jid, password, nil, nil, nil, nil, workqueue_count: 0)
+
+ EM.error_handler { |e| warn e.message }
+ Thread.new do
+ EM.run do
+ client.run
+ end
+ end
+
+ at_exit { shutdown }
+
+ Timeout.timeout(5) { @ready.pop }
+ end
+end