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