Merge branch 'feature_flags'

Stephen Paul Weber created

* feature_flags:
  Put CDRs behind a feature flag
  Store feature flags on user for limiting commands, etc
  Added a New Command to Display CDRs to Customers

Change summary

.rubocop.yml          |  2 
forms/customer_cdr.rb | 13 ++++++++++++
lib/cdr.rb            | 28 ++++++++++++++++++--------
lib/cdr_repo.rb       | 47 +++++++++++++++++++++++++++++++++++++++++++++
lib/command_list.rb   |  2 
lib/customer.rb       |  6 +++-
lib/customer_repo.rb  | 12 +++++++---
sgx_jmp.rb            | 16 +++++++++++++++
test/test_helper.rb   |  4 +++
web.rb                |  9 ++++++-
10 files changed, 120 insertions(+), 19 deletions(-)

Detailed changes

.rubocop.yml ๐Ÿ”—

@@ -24,7 +24,7 @@ Metrics/AbcSize:
     - test/*
 
 Metrics/ParameterLists:
-  Max: 6
+  Max: 7
 
 Naming/MethodParameterName:
   AllowNamesEndingInNumbers: false

forms/customer_cdr.rb ๐Ÿ”—

@@ -0,0 +1,13 @@
+result!
+title "Call History"
+
+table(
+	@cdrs,
+	start: "Start",
+	direction: "Direction",
+	tel: "Number",
+	disposition: "Status",
+	duration: "Duration",
+	formatted_rate: "Rate",
+	formatted_charge: "Charge"
+)

lib/cdr.rb ๐Ÿ”—

@@ -30,7 +30,25 @@ class CDR
 		billsec Integer
 		disposition Disposition
 		tel(/\A\+\d+\Z/)
-		direction Either(:inbound, :outbound)
+		direction Either(:inbound, :outbound), coerce: :to_sym.to_proc
+		rate Either(nil, BigDecimal), default: nil
+		charge Either(nil, BigDecimal), default: nil
+	end
+
+	def formatted_rate
+		"$%.4f" % rate
+	end
+
+	def formatted_charge
+		"$%.4f" % charge
+	end
+
+	def duration
+		"%02d:%02d:%02d" % [
+			billsec / (60 * 60),
+			billsec % (60 * 60) / 60,
+			billsec % 60
+		]
 	end
 
 	def self.for(event, **kwargs)
@@ -61,12 +79,4 @@ class CDR
 			direction: :outbound
 		)
 	end
-
-	def save
-		columns, values = to_h.to_a.transpose
-		DB.query_defer(<<~SQL, values)
-			INSERT INTO cdr (#{columns.join(',')})
-			VALUES ($1, $2, $3, $4, $5, $6, $7)
-		SQL
-	end
 end

lib/cdr_repo.rb ๐Ÿ”—

@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require_relative "cdr"
+
+class CDRRepo
+	def initialize(db: DB)
+		@db = db
+	end
+
+	def put(cdr)
+		data = cdr.to_h
+		data.delete(:rate)
+		data.delete(:charge)
+		columns, values = data.to_a.transpose
+		DB.query_defer(<<~SQL, values)
+			INSERT INTO cdr (#{columns.join(',')})
+			VALUES ($1, $2, $3, $4, $5, $6, $7)
+		SQL
+	end
+
+	def find_range(customer, range)
+		customer_id = customer.customer_id
+		cdrs = @db.query_defer(CDR_SQL, [customer_id, range.first, range.last])
+
+		cdrs.then do |rows|
+			rows.map { |row|
+				CDR.new(**row.transform_keys(&:to_sym))
+			}
+		end
+	end
+
+	CDR_SQL = <<~SQL
+		SELECT
+			cdr_id,
+			customer_id,
+			start,
+			billsec,
+			disposition,
+			tel,
+			direction,
+			rate,
+			charge
+		FROM cdr_with_charge
+		WHERE customer_id = $1 AND start >= $2 AND DATE_TRUNC('day', start) <= $3
+		ORDER BY start DESC
+	SQL
+end

lib/command_list.rb ๐Ÿ”—

@@ -18,7 +18,7 @@ class CommandList
 		args = {
 			from_jid: from_jid, customer: customer,
 			tel: customer&.registered? ? customer&.registered?&.phone : nil,
-			fwd: customer&.fwd,
+			fwd: customer&.fwd, feature_flags: customer&.feature_flags || [],
 			payment_methods: []
 		}
 		return EMPromise.resolve(args) unless customer&.plan_name

lib/customer.rb ๐Ÿ”—

@@ -18,7 +18,7 @@ require_relative "./trivial_backend_sgx_repo"
 class Customer
 	extend Forwardable
 
-	attr_reader :customer_id, :balance, :jid, :tndetails
+	attr_reader :customer_id, :balance, :jid, :tndetails, :feature_flags
 	alias billing_customer_id customer_id
 
 	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
@@ -43,7 +43,7 @@ class Customer
 		klass.new(
 			customer_id, jid,
 			plan: CustomerPlan.extract(customer_id, **kwargs),
-			**kwargs.slice(:balance, :sgx, :tndetails, *keys)
+			**kwargs.slice(:balance, :sgx, :tndetails, :feature_flags, *keys)
 		)
 	end
 
@@ -53,6 +53,7 @@ class Customer
 		plan: CustomerPlan.new(customer_id),
 		balance: BigDecimal(0),
 		tndetails: {},
+		feature_flags: [],
 		sgx: TrivialBackendSgxRepo.new.get(customer_id)
 	)
 		@plan = plan
@@ -62,6 +63,7 @@ class Customer
 		@jid = jid
 		@balance = balance
 		@tndetails = tndetails
+		@feature_flags = feature_flags
 		@sgx = sgx
 	end
 

lib/customer_repo.rb ๐Ÿ”—

@@ -164,11 +164,15 @@ protected
 	end
 
 	def fetch_redis(customer_id)
-		mget(
-			"jmp_customer_auto_top_up_amount-#{customer_id}",
-			"jmp_customer_monthly_overage_limit-#{customer_id}"
-		).then { |r|
+		EMPromise.all([
+			mget(
+				"jmp_customer_auto_top_up_amount-#{customer_id}",
+				"jmp_customer_monthly_overage_limit-#{customer_id}"
+			),
+			@redis.smembers("jmp_customer_feature_flags-#{customer_id}")
+		]).then { |r, flags|
 			r.transform_keys { |k| k.match(/^jmp_customer_([^-]+)/)[1].to_sym }
+			 .merge(feature_flags: flags.map(&:to_sym))
 		}
 	end
 

sgx_jmp.rb ๐Ÿ”—

@@ -526,6 +526,22 @@ Command.new(
 	end
 }.register(self).then(&CommandList.method(:register))
 
+Command.new(
+	"cdrs",
+	"๐Ÿ“ฒ Show Call Logs",
+	list_for: ->(feature_flags:, **) { feature_flags.include?(:cdrs) }
+) {
+	report_for = ((Date.today << 1)..Date.today)
+
+	Command.customer.then { |customer|
+		CDRRepo.new.find_range(customer, report_for)
+	}.then do |cdrs|
+		Command.finish do |reply|
+			reply.command << FormTemplate.render("customer_cdr", cdrs: cdrs)
+		end
+	end
+}.register(self).then(&CommandList.method(:register))
+
 Command.new(
 	"transactions",
 	"๐Ÿงพ Show Transactions",

test/test_helper.rb ๐Ÿ”—

@@ -258,6 +258,10 @@ class FakeRedis
 		@values[key]&.size || 0
 	end
 
+	def smembers(key)
+		@values[key]&.to_a || []
+	end
+
 	def expire(_, _); end
 
 	def exists(*keys)

web.rb ๐Ÿ”—

@@ -10,6 +10,7 @@ require "sentry-ruby"
 
 require_relative "lib/call_attempt_repo"
 require_relative "lib/cdr"
+require_relative "lib/cdr_repo"
 require_relative "lib/oob"
 require_relative "lib/rev_ai"
 require_relative "lib/roda_capture"
@@ -121,6 +122,10 @@ class Web < Roda
 		opts[:call_attempt_repo] || CallAttemptRepo.new
 	end
 
+	def cdr_repo
+		opts[:cdr_repo] || CDRRepo.new
+	end
+
 	def rev_ai
 		RevAi.new(logger: log.child(loggable_params))
 	end
@@ -200,7 +205,7 @@ class Web < Roda
 						end
 
 						customer_repo.find_by_tel(params["to"]).then do |customer|
-							CDR.for_inbound(customer.customer_id, params).save
+							cdr_repo.put(CDR.for_inbound(customer.customer_id, params))
 						end
 					end
 					"OK"
@@ -359,7 +364,7 @@ class Web < Roda
 					log.info "#{params['eventType']} #{params['callId']}", loggable_params
 					if params["eventType"] == "disconnect"
 						call_attempt_repo.ending_call(c, params["callId"])
-						CDR.for_outbound(params).save.catch(&method(:log_error))
+						cdr_repo.put(CDR.for_outbound(params)).catch(&method(:log_error))
 					end
 					"OK"
 				end