#!/usr/bin/ruby
# frozen_string_literal: true
# Usage: ./billing_monthly_cronjob '{
# notify_using = {
# jid = "",
# password = "",
# target = \(jid: Text) -> "+12266669977@cheogram.com",
# body = \(jid: Text) -> \(body: Text) -> "/msg ${jid} ${body}",
# },
# plans = ./plans.dhall
# }'
require "bigdecimal"
require "date"
require "dhall"
require "net/http"
require "pg"
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,
body: Text -> Text -> Text
},
plans: List {
name: Text,
currency: Currency,
monthly_price: Natural,
minutes: Quota,
messages: Quota
}
}
DHALL
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)
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 }
new(plan) if plan
end
def initialize(plan)
@plan = plan
end
def price
BigDecimal.new(@plan["monthly_price"].to_i) * 0.0001
end
def bill_customer(db, customer_id)
transaction_id = "#{customer_id}-renew-until-#{RENEW_UNTIL}"
db.exec_params(<<-SQL, [customer_id, transaction_id, -price])
INSERT INTO transactions
(customer_id, transaction_id, amount, note)
VALUES
($1, $2, $3, 'Renew account plan')
SQL
end
def renew(db, customer_id, expires_at)
bill_customer(db, customer_id)
params = [RENEW_UNTIL, customer_id, expires_at]
db.exec_params(<<-SQL, params)
UPDATE plan_log
SET date_range=range_merge(date_range, tsrange('now', $1))
WHERE customer_id=$2 AND date_range -|- tsrange($3, $3, '[]')
SQL
end
end
class ExpiredCustomer
def self.for(row, db)
plan = Plan.from_name(row["plan_name"])
if row["balance"] < plan.price
WithLowBalance.new(row, plan, db)
else
new(row, plan, db)
end
end
def initialize(row, plan, db)
@row = row
@plan = plan
@db = db
end
def customer_id
@row["customer_id"]
end
def try_renew(db, stats)
@plan.renew(
db,
customer_id,
@row["expires_at"]
)
stats.add(:renewed, 1)
stats.add(:revenue, @plan.price)
end
class WithLowBalance < ExpiredCustomer
ONE_WEEK = 60 * 60 * 24 * 7
LAST_WEEK = Time.now - ONE_WEEK
def try_renew(_, stats)
stats.add(:not_renewed, 1)
if REDIS.exists?("jmp_customer_auto_top_up_amount-#{customer_id}") && \
@row["expires_at"] > LAST_WEEK
@db.exec_params("SELECT pg_notify('low_balance', $1)", [customer_id])
else
return if REDIS.exists?("jmp_customer_low_balance-#{customer_id}")
REDIS.set("jmp_customer_low_balance-#{customer_id}", Time.now, ex: ONE_WEEK)
send_notification
end
end
protected
def jid
REDIS.get("jmp_customer_jid-#{customer_id}")
end
def tel
REDIS.lindex("catapult_cred-#{jid}", 3)
end
def btc_addresses
@btc_addresses ||= REDIS.smembers(
"jmp_customer_btc_addresses-#{customer_id}"
)
end
def btc_addresses_for_notification
return if btc_addresses.empty?
"\nYou can buy credit by sending any amount of Bitcoin to one of "\
"these addresses:\n#{btc_addresses.join("\n")}"
end
def send_notification
raise "No JID for #{customer_id}, cannot notify" unless jid
BlatherNotify.say(
CONFIG[:notify_using][:target].call(jid),
CONFIG[:notify_using][:body].call(
jid, renewal_notification
)
)
end
def renewal_notification
"Failed to renew account for #{tel}, " \
"balance of $#{'%.4f' % @row['balance']} is too low. " \
"To keep your number, please buy more credit soon. " \
"#{btc_addresses_for_notification}"
end
end
end
db.transaction do
db.exec(
<<-SQL
SELECT customer_id, plan_name, expires_at, COALESCE(balance, 0) AS balance
FROM customer_plans LEFT JOIN balances USING (customer_id)
WHERE expires_at <= NOW()
SQL
).each do |row|
ExpiredCustomer.for(row, db).try_renew(db, stats)
end
end
p stats