@@ 1,52 1,26 @@
#!/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"
require_relative "../lib/to_form"
-using ToForm
-
CONFIG = Dhall.load(<<-DHALL).sync
- let Quota = < unlimited | limited: { included: Natural, price: Natural } >
- let Currency = < CAD | USD >
- in
(#{ARGV[0]}) : {
- healthchecks_url: Text,
sgx_jmp: 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
+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)
@@ 56,193 30,62 @@ BlatherNotify.start(
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
+promises = []
+
+db.exec(
+ <<-SQL
+ SELECT customer_id
+ FROM customer_plans
+ WHERE expires_at <= LOCALTIMESTAMP + '4 days'
+ SQL
+).each do |row|
+ EM.next_tick do
+ promises << BlatherNotify.execute(
+ "customer info",
+ { q: row["customer_id"] }.to_form(:submit)
+ ).then { |iq|
+ BlatherNotify.write_with_promise(BlatherNotify.command(
+ "customer info",
+ iq.sessionid
+ ))
+ }.then do |iq|
+ unless iq.form.field("action")
+ next "#{row["customer_id"]} not found"
+ end
- def to_h
- @stats.transform_values { |v| v.is_a?(BigDecimal) ? v.to_s("F") : v }
+ BlatherNotify.write_with_promise(BlatherNotify.command(
+ "customer info",
+ iq.sessionid,
+ action: :complete,
+ form: { action: "bill_plan" }.to_form(:submit)
+ ))
+ end
end
end
-stats = Stats.new(
- not_renewed: 0,
- renewed: 0,
- not_registered: 0,
- revenue: BigDecimal(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
+one = Queue.new
- def initialize(plan)
- @plan = plan
- end
-
- def price
- BigDecimal(@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, settled_after, amount, note)
- VALUES
- ($1, $2, LOCALTIMESTAMP, $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
+def format(item)
+ if item.respond_to?(:note) && item.note
+ item.note.text
+ elsif item.respond_to?(:to_xml)
+ item.to_xml
+ else
+ item.inspect
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)
- topup = "jmp_customer_auto_top_up_amount-#{customer_id}"
- if REDIS.exists?(topup) && @row["expires_at"] > LAST_WEEK
- @db.exec_params(
- "SELECT pg_notify('low_balance', $1)",
- [customer_id]
- )
- else
- notify_if_needed
- end
- end
-
- protected
-
- def notify_if_needed
- 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
-
- def jid
- REDIS.get("jmp_customer_jid-#{customer_id}")
- end
-
- def tel
- REDIS.lindex("catapult_cred-customer_#{customer_id}@jmp.chat", 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
+EM.add_timer(0) do
+ EMPromise.all(promises).then(
+ ->(all) { one << all },
+ ->(err) { one << RuntimeError.new(format(err)) }
+ )
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|
- one = Queue.new
- EM.next_tick do
- BlatherNotify.execute(
- "customer info",
- { q: row["customer_id"] }.to_form(:submit)
- ).then(
- ->(x) { one << x },
- ->(e) { one << RuntimeError.new(e.to_s) }
- )
- end
- info = one.pop
- raise info if info.is_a?(Exception)
+result = one.pop
- if info.form.field("tel")&.value
- ExpiredCustomer.for(row, db).try_renew(db, stats)
- else
- stats.add(:not_registered, 1)
- end
- end
-end
+raise result if result.is_a?(Exception)
-p stats
+result.each do |item|
+ puts format(item)
+end