From e7b975190d75cff8402579e8c9b0fc02f4ab2db2 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 18 Apr 2022 13:48:48 -0500 Subject: [PATCH] Billing monthly cronjob using sgx-jmp Just get the list of expired customers and tell sgx-jmp about each of them, wait until all return or one errors and log result. --- bin/billing_monthly_cronjob | 255 +++++++----------------------------- lib/blather_notify.rb | 4 + 2 files changed, 53 insertions(+), 206 deletions(-) diff --git a/bin/billing_monthly_cronjob b/bin/billing_monthly_cronjob index ab6aeeb..b603fb1 100755 --- a/bin/billing_monthly_cronjob +++ b/bin/billing_monthly_cronjob @@ -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 diff --git a/lib/blather_notify.rb b/lib/blather_notify.rb index ba64e96..a6e0adc 100644 --- a/lib/blather_notify.rb +++ b/lib/blather_notify.rb @@ -42,6 +42,10 @@ module BlatherNotify def self.write_with_promise(stanza) promise = EMPromise.new + EM.add_timer(15) do + promise.reject(:timeout) + end + client.write_with_handler(stanza) do |s| if s.error? promise.reject(s) -- 2.38.5