~singpolyma/jmp-pay

7c438f83822c6a2f41da32b93bbaa48203f0932b — Stephen Paul Weber 1 year, 5 months ago 37b09a4
Notify customer when renewal fails due to low balance

This is done by sending from a configured JID to <tel>@cheogram.com in order to
have them receive a message from support.
3 files changed, 136 insertions(+), 29 deletions(-)

M Gemfile
M bin/billing_monthly_cronjob
A lib/blather_notify.rb
M Gemfile => Gemfile +1 -0
@@ 2,6 2,7 @@

source "https://rubygems.org"

gem "blather"
gem "braintree"
gem "dhall"
gem "money-open-exchange-rates"

M bin/billing_monthly_cronjob => bin/billing_monthly_cronjob +107 -29
@@ 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)

A lib/blather_notify.rb => lib/blather_notify.rb +28 -0
@@ 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