~singpolyma/sgx-jmp

8fd61ce689c290b5bd47253f1e0b9b6d67cc6eba — Stephen Paul Weber 8 months ago ba03284 + 3b7abeb
Merge branch 'monthly-billing'

* monthly-billing:
  Allow the DB to notify us to bill a customer
  Command.execution setter
  Bill plan command
  Clearer name for lock bypass factory
M forms/admin_menu.rb => forms/admin_menu.rb +2 -1
@@ 9,6 9,7 @@ field(
	description: "or put a new customer info",
	options: [
		{ value: "info", label: "Customer Info" },
		{ value: "financial", label: "Customer Billing Information" }
		{ value: "financial", label: "Customer Billing Information" },
		{ value: "bill_plan", label: "Bill Customer" }
	]
)

M lib/admin_command.rb => lib/admin_command.rb +5 -0
@@ 1,5 1,6 @@
# frozen_string_literal: true

require_relative "bill_plan_command"
require_relative "customer_info_form"
require_relative "financial_info"
require_relative "form_template"


@@ 65,6 66,10 @@ class AdminCommand
		end
	end

	def action_bill_plan
		BillPlanCommand.for(@target_customer).call
	end

	def pay_methods(financial_info)
		reply(FormTemplate.render(
			"admin_payment_methods",

A lib/bill_plan_command.rb => lib/bill_plan_command.rb +69 -0
@@ 0,0 1,69 @@
# frozen_string_literal: true

class BillPlanCommand
	def self.for(customer)
		return ForUnregistered.new unless customer.registered?

		unless customer.balance > customer.monthly_price
			return ForLowBalance.new(customer)
		end

		new(customer)
	end

	def initialize(customer)
		@customer = customer
	end

	def call
		@customer.bill_plan
		Command.reply do |reply|
			reply.note_type = :info
			reply.note_text = "Customer billed"
		end
	end

	class ForLowBalance
		def initialize(customer)
			@customer = customer
		end

		def call
			LowBalance.for(@customer).then(&:notify!).then do |amount|
				return command_for(amount).call if amount&.positive?

				notify_failure
				Command.reply do |reply|
					reply.note_type = :error
					reply.note_text = "Customer balance is too low"
				end
			end
		end

	protected

		def notify_failure
			m = Blather::Stanza::Message.new
			m.from = CONFIG[:notify_from]
			m.body =
				"Failed to renew account for #{@customer.registered?.phone}. " \
				"To keep your number, please buy more credit soon."
			@customer.stanza_to(m)
		end

		def command_for(amount)
			BillPlanCommand.for(
				@customer.with_balance(@customer.balance + amount)
			)
		end
	end

	class ForUnregistered
		def call
			Command.reply do |reply|
				reply.note_type = :error
				reply.note_text = "Customer is not registered"
			end
		end
	end
end

M lib/command.rb => lib/command.rb +5 -1
@@ 11,6 11,10 @@ class Command
		Thread.current[:execution]
	end

	def self.execution=(exe)
		Thread.current[:execution] = exe
	end

	def self.reply(stanza=nil, &blk)
		execution.reply(stanza, &blk)
	end


@@ 51,7 55,7 @@ class Command
		def execute
			StatsD.increment("command", tags: ["node:#{iq.node}"])
			EMPromise.resolve(nil).then {
				Thread.current[:execution] = self
				Command.execution = self
				sentry_hub
				catch_after(EMPromise.resolve(yield self))
			}.catch(&method(:panic))

M lib/customer.rb => lib/customer.rb +2 -1
@@ 24,7 24,8 @@ class Customer

	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
	               :currency, :merchant_account, :plan_name, :minute_limit,
	               :message_limit, :auto_top_up_amount, :monthly_overage_limit
	               :message_limit, :auto_top_up_amount, :monthly_overage_limit,
	               :monthly_price
	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
	               :fwd, :transcription_enabled
	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage

A lib/db_notification.rb => lib/db_notification.rb +17 -0
@@ 0,0 1,17 @@
# frozen_string_literal: true

require_relative "dummy_command"

module DbNotification
	def self.for(notify, customer)
		case notify[:relname]
		when "low_balance"
			LowBalance.for(customer).then { |lb| lb.method(:notify!) }
		when "possible_renewal"
			Command.execution = DummyCommand.new(customer)
			BillPlanCommand.for(customer)
		else
			raise "Unknown notification: #{notify[:relname]}"
		end
	end
end

A lib/dummy_command.rb => lib/dummy_command.rb +17 -0
@@ 0,0 1,17 @@
# frozen_string_literal: true

class DummyCommand
	attr_reader :customer

	def initialize(customer)
		@customer = customer
	end

	def reply(*); end

	def finish(*); end

	def log
		::LOG
	end
end

M lib/low_balance.rb => lib/low_balance.rb +2 -2
@@ 11,11 11,11 @@ class LowBalance
			"jmp_customer_low_balance-#{customer.customer_id}",
			expiry: 60 * 60 * 24 * 7
		).with(-> { Locked.new }) do
			for_auto_top_up_amount(customer)
			for_no_lock(customer)
		end
	end

	def self.for_auto_top_up_amount(customer)
	def self.for_no_lock(customer)
		if customer.auto_top_up_amount.positive?
			AutoTopUp.new(customer)
		else

M sgx_jmp.rb => sgx_jmp.rb +23 -5
@@ 81,6 81,8 @@ require_relative "lib/command_list"
require_relative "lib/customer"
require_relative "lib/customer_info_form"
require_relative "lib/customer_repo"
require_relative "lib/dummy_command"
require_relative "lib/db_notification"
require_relative "lib/electrum"
require_relative "lib/empty_repo"
require_relative "lib/expiring_lock"


@@ 178,10 180,27 @@ end

EM.error_handler(&method(:panic))

# Infer anything we might have been notified about while we were down
def catchup_notify(db)
	db.query("SELECT customer_id FROM balances WHERE balance < 5").each do |c|
		db.query("SELECT pg_notify('low_balance', $1)", c.values)
	end
	db.query(<<~SQL).each do |c|
		SELECT customer_id
		FROM customer_plans INNER JOIN balances USING (customer_id)
		WHERE expires_at < LOCALTIMESTAMP AND balance >= 5
	SQL
		db.query("SELECT pg_notify('possible_renewal', $1)", c.values)
	end
end

def poll_for_notify(db)
	db.wait_for_notify_defer.then { |notify|
		CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new).find(notify[:extra])
	}.then(&LowBalance.method(:for)).then(&:notify!).then {
		CustomerRepo
			.new(sgx_repo: Bwmsgsv2Repo.new)
			.find(notify[:extra])
			.then { |customer| DbNotification.for(notify, customer) }
	}.then(&:call).then {
		poll_for_notify(db)
	}.catch(&method(:panic))
end


@@ 208,9 227,8 @@ when_ready do

	DB.hold do |conn|
		conn.query("LISTEN low_balance")
		conn.query("SELECT customer_id FROM balances WHERE balance < 5").each do |c|
			conn.query("SELECT pg_notify('low_balance', $1)", c.values)
		end
		conn.query("LISTEN possible_renewal")
		catchup_notify(conn)
		poll_for_notify(conn)
	end