customer_repo.rb

  1# frozen_string_literal: true
  2
  3require "lazy_object"
  4require "value_semantics/monkey_patched"
  5
  6require_relative "bandwidth_tn_repo"
  7require_relative "customer"
  8require_relative "polyfill"
  9
 10class CustomerRepo
 11	class NotFound < RuntimeError; end
 12
 13	value_semantics do
 14		set_user          Proc,       default: ->(**) {}, coerce: :to_proc.to_proc
 15		redis             Anything(), default: LazyObject.new { REDIS }
 16		db                Anything(), default: LazyObject.new { DB }
 17		braintree         Anything(), default: LazyObject.new { BRAINTREE }
 18		sgx_repo          Anything(), default: TrivialBackendSgxRepo.new
 19		bandwidth_tn_repo Anything(), default: BandwidthTnRepo.new
 20	end
 21
 22	module QueryKey
 23		def self.for(s)
 24			case s
 25			when Blather::JID, ProxiedJID
 26				JID.for(s)
 27			when /\Axmpp:(.*)/
 28				JID.for($1)
 29			when /\A(?:tel:)?(\+\d+)\Z/
 30				Tel.new($1)
 31			else
 32				ID.new(s)
 33			end
 34		end
 35
 36		ID = Struct.new(:customer_id) do
 37			def keys(redis, tel: nil)
 38				redis.get("jmp_customer_jid-#{customer_id}").then do |jid|
 39					raise NotFound, "No jid" unless jid
 40
 41					[customer_id, Blather::JID.new(jid), *tel]
 42				end
 43			end
 44		end
 45
 46		JID = Struct.new(:jid) do
 47			def self.for(jid)
 48				norm = jid.to_s.downcase
 49				if norm =~ /\Acustomer_(.+)@#{CONFIG[:component][:jid]}\Z/
 50					ID.new($1)
 51				else
 52					new(norm)
 53				end
 54			end
 55
 56			def keys(redis, tel: nil)
 57				redis.get("jmp_customer_id-#{jid}").then do |customer_id|
 58					raise NotFound, "No customer" unless customer_id
 59
 60					[customer_id, Blather::JID.new(jid), *tel]
 61				end
 62			end
 63		end
 64
 65		Tel = Struct.new(:tel) do
 66			def keys(redis)
 67				redis.get("catapult_jid-#{tel}").then do |jid|
 68					raise NotFound, "No jid for '#{tel}'" unless jid.to_s =~ /\Acustomer_/
 69
 70					JID.for(jid).keys(redis, tel: tel)
 71				end
 72			end
 73		end
 74	end
 75
 76	def find(customer_id)
 77		set_user.call(customer_id: customer_id)
 78		QueryKey::ID.new(customer_id).keys(redis).then { |k| find_inner(*k) }
 79	end
 80
 81	def find_by_jid(jid)
 82		set_user.call(jid: jid)
 83		QueryKey::JID.for(jid).keys(redis).then { |k| find_inner(*k) }
 84	end
 85
 86	def find_by_tel(tel)
 87		set_user.call(tel: tel)
 88		QueryKey::Tel.new(tel).keys(redis).then { |k| find_inner(*k) }
 89	end
 90
 91	def find_by_format(s)
 92		set_user.call(q: s)
 93		QueryKey.for(s).keys(redis).then { |k| find_inner(*k) }
 94	end
 95
 96	def create(jid)
 97		@braintree.customer.create.then do |result|
 98			raise "Braintree customer create failed" unless result.success?
 99
100			c, norm = result.customer.id, jid.to_s.downcase
101			@redis.msetnx(
102				"jmp_customer_id-#{norm}", c, "jmp_customer_jid-#{c}", norm
103			).then do |redis_result|
104				raise "Saving new customer to redis failed" unless redis_result == 1
105
106				mksgx(c).then { |sgx| Customer.created(c, jid, sgx: sgx, repo: self) }
107			end
108		end
109	end
110
111	def disconnect_tel(customer)
112		tel = customer.registered?.phone
113		@bandwidth_tn_repo.disconnect(tel, customer.customer_id)
114	end
115
116	def put_lidb_name(customer, lidb_name)
117		@bandwidth_tn_repo.put_lidb_name(customer.registered?.phone, lidb_name)
118	end
119
120	def put_transcription_enabled(customer, enabled)
121		@sgx_repo.put_transcription_enabled(customer.customer_id, enabled)
122	end
123
124	def put_fwd(customer, customer_fwd)
125		tel = customer.registered?.phone
126		@sgx_repo.put_fwd(customer.customer_id, tel, customer_fwd)
127	end
128
129	def put_monthly_limits(customer, **kwargs)
130		@redis.mset(*kwargs.flat_map { |(k, v)|
131			["jmp_customer_#{k}-#{customer.customer_id}", v.to_i]
132		})
133	end
134
135	def change_jid(customer, new_jid)
136		norm_jid = new_jid.to_s.downcase
137		@redis.set("jmp_customer_id-#{norm_jid}", customer.customer_id).then {
138			@redis.set("jmp_customer_jid-#{customer.customer_id}", norm_jid)
139		}.then { SwapDefaultFwd.new.do(self, customer, new_jid) }.then do
140			@redis.del("jmp_customer_id-#{customer.jid}")
141		end
142	end
143
144	# I've put this here to hide the lines from rubocop
145	# After we sort out where call routing should live, this whole process will
146	# no longer be necessary
147	class SwapDefaultFwd
148		def do(repo, customer, new_jid)
149			unless customer.fwd.uri == "xmpp:#{customer.jid}"
150				return EMPromise.resolve(nil)
151			end
152
153			repo.put_fwd(customer, customer.fwd.with(uri: "xmpp:#{new_jid}"))
154		end
155	end
156
157protected
158
159	def mksgx(cid)
160		TrivialBackendSgxRepo.get_unregistered(cid, redis: redis)
161	end
162
163	def mget(*keys)
164		@redis.mget(*keys).then { |values| Hash[keys.zip(values.map(&:to_i))] }
165	end
166
167	def fetch_redis(customer_id)
168		EMPromise.all([
169			mget(
170				"jmp_customer_auto_top_up_amount-#{customer_id}",
171				"jmp_customer_monthly_overage_limit-#{customer_id}"
172			),
173			@redis.smembers("jmp_customer_feature_flags-#{customer_id}")
174		]).then { |r, flags|
175			r.transform_keys { |k| k.match(/^jmp_customer_([^-]+)/)[1].to_sym }
176			 .merge(feature_flags: flags.map(&:to_sym))
177		}
178	end
179
180	SQL = <<~SQL
181		SELECT
182			COALESCE(balance,0) AS balance, cust.plan_name, cust.expires_at,
183			cust.parent_customer_id, cust.pending, parent.plan_name AS parent_plan_name
184		FROM customer_plans cust LEFT JOIN balances USING (customer_id)
185		LEFT JOIN customer_plans parent ON parent.customer_id=cust.parent_customer_id
186		WHERE cust.customer_id=$1 LIMIT 1
187	SQL
188
189	def tndetails(sgx)
190		return {} unless sgx.registered?
191
192		LazyObject.new { @bandwidth_tn_repo.find(sgx.registered?.phone) || {} }
193	end
194
195	def find_inner(cid, jid, tel=nil)
196		set_user.call(customer_id: cid, jid: jid)
197		EMPromise.all([
198			@sgx_repo.get(cid, tel: tel).then { |sgx| { sgx: sgx } },
199			@db.query_one(SQL, cid, default: {}), fetch_redis(cid)
200		]).then { |all| all.reduce(&:merge) }.then do |data|
201			Customer.extract(cid, jid, tndetails: tndetails(data[:sgx]), **data)
202		end
203	end
204end