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)
 38				redis.get("jmp_customer_jid-#{customer_id}").then do |jid|
 39					raise NotFound, "No jid" unless jid
 40
 41					[customer_id, jid]
 42				end
 43			end
 44		end
 45
 46		JID = Struct.new(:jid) do
 47			def self.for(jid)
 48				if jid.to_s =~ /\Acustomer_(.+)@#{CONFIG[:component][:jid]}\Z/
 49					ID.new($1)
 50				else
 51					new(jid)
 52				end
 53			end
 54
 55			def keys(redis)
 56				redis.get("jmp_customer_id-#{jid}").then do |customer_id|
 57					raise NotFound, "No customer" unless customer_id
 58
 59					[customer_id, jid]
 60				end
 61			end
 62		end
 63
 64		Tel = Struct.new(:tel) do
 65			def keys(redis)
 66				redis.get("catapult_jid-#{tel}").then do |jid|
 67					raise NotFound, "No jid" unless jid
 68
 69					JID.for(jid).keys(redis)
 70				end
 71			end
 72		end
 73	end
 74
 75	def find(customer_id)
 76		set_user.call(customer_id: customer_id)
 77		QueryKey::ID.new(customer_id).keys(redis).then { |k| find_inner(*k) }
 78	end
 79
 80	def find_by_jid(jid)
 81		set_user.call(jid: jid)
 82		QueryKey::JID.for(jid).keys(redis).then { |k| find_inner(*k) }
 83	end
 84
 85	def find_by_tel(tel)
 86		set_user.call(tel: tel)
 87		QueryKey::Tel.new(tel).keys(redis).then { |k| find_inner(*k) }
 88	end
 89
 90	def find_by_format(s)
 91		set_user.call(q: s)
 92		QueryKey.for(s).keys(redis).then { |k| find_inner(*k) }
 93	end
 94
 95	def create(jid)
 96		@braintree.customer.create.then do |result|
 97			raise "Braintree customer create failed" unless result.success?
 98
 99			cid = result.customer.id
100			@redis.msetnx(
101				"jmp_customer_id-#{jid}", cid, "jmp_customer_jid-#{cid}", jid
102			).then do |redis_result|
103				raise "Saving new customer to redis failed" unless redis_result == 1
104
105				Customer.new(cid, Blather::JID.new(jid), sgx: new_sgx(cid))
106			end
107		end
108	end
109
110	def put_lidb_name(customer, lidb_name)
111		@bandwidth_tn_repo.put_lidb_name(customer.registered?.phone, lidb_name)
112	end
113
114	def put_transcription_enabled(customer, enabled)
115		@sgx_repo.put_transcription_enabled(customer.customer_id, enabled)
116	end
117
118	def put_fwd(customer, customer_fwd)
119		tel = customer.registered?.phone
120		@sgx_repo.put_fwd(customer.customer_id, tel, customer_fwd)
121	end
122
123	def put_monthly_overage_limit(customer, limit)
124		k = "jmp_customer_monthly_overage_limit-#{customer.customer_id}"
125		@redis.set(k, limit)
126	end
127
128protected
129
130	def new_sgx(customer_id)
131		TrivialBackendSgxRepo.new.get(customer_id).with(registered?: false)
132	end
133
134	def mget(*keys)
135		@redis.mget(*keys).then { |values| Hash[keys.zip(values.map(&:to_i))] }
136	end
137
138	def fetch_redis(customer_id)
139		mget(
140			"jmp_customer_auto_top_up_amount-#{customer_id}",
141			"jmp_customer_monthly_overage_limit-#{customer_id}"
142		).then { |r|
143			r.transform_keys { |k| k.match(/^jmp_customer_([^-]+)/)[1].to_sym }
144		}
145	end
146
147	SQL = <<~SQL
148		SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
149		FROM customer_plans LEFT JOIN balances USING (customer_id)
150		WHERE customer_id=$1 LIMIT 1
151	SQL
152
153	def fetch_sql(customer_id)
154		@db.query_defer(SQL, [customer_id]).then do |rows|
155			rows.first&.transform_keys(&:to_sym) || {}
156		end
157	end
158
159	def fetch_all(customer_id)
160		EMPromise.all([
161			@sgx_repo.get(customer_id),
162			fetch_sql(customer_id),
163			fetch_redis(customer_id)
164		]).then { |sgx, sql, redis| [sgx, sql.merge(redis)] }
165	end
166
167	def tndetails(sgx)
168		return {} unless sgx.registered?
169
170		LazyObject.new { @bandwidth_tn_repo.find(sgx.registered?.phone) || {} }
171	end
172
173	def find_inner(customer_id, jid)
174		set_user.call(customer_id: customer_id, jid: jid)
175		fetch_all(customer_id).then do |(sgx, data)|
176			Customer.new(
177				customer_id, Blather::JID.new(jid),
178				sgx: sgx, tndetails: tndetails(sgx), plan: CustomerPlan.for(
179					customer_id,
180					**data.reject { |(k, _)| k == :balance }
181				), **data.slice(:balance)
182			)
183		end
184	end
185end