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 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, tel: nil)
56 redis.get("jmp_customer_id-#{jid}").then do |customer_id|
57 raise NotFound, "No customer" unless customer_id
58
59 [customer_id, Blather::JID.new(jid), *tel]
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 for '#{tel}'" unless jid.to_s =~ /\Acustomer_/
68
69 JID.for(jid).keys(redis, tel: tel)
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 c = result.customer.id
100 @redis.msetnx(
101 "jmp_customer_id-#{jid}", c, "jmp_customer_jid-#{c}", jid
102 ).then do |redis_result|
103 raise "Saving new customer to redis failed" unless redis_result == 1
104
105 mksgx(c).then { |sgx| Customer.created(c, jid, sgx: sgx, repo: self) }
106 end
107 end
108 end
109
110 def disconnect_tel(customer)
111 tel = customer.registered?.phone
112 @bandwidth_tn_repo.disconnect(tel, customer.customer_id)
113 end
114
115 def put_lidb_name(customer, lidb_name)
116 @bandwidth_tn_repo.put_lidb_name(customer.registered?.phone, lidb_name)
117 end
118
119 def put_transcription_enabled(customer, enabled)
120 @sgx_repo.put_transcription_enabled(customer.customer_id, enabled)
121 end
122
123 def put_fwd(customer, customer_fwd)
124 tel = customer.registered?.phone
125 @sgx_repo.put_fwd(customer.customer_id, tel, customer_fwd)
126 end
127
128 def put_monthly_limits(customer, **kwargs)
129 @redis.mset(*kwargs.flat_map { |(k, v)|
130 ["jmp_customer_#{k}-#{customer.customer_id}", v.to_i]
131 })
132 end
133
134 def change_jid(customer, new_jid)
135 @redis.set("jmp_customer_id-#{new_jid}", customer.customer_id).then {
136 @redis.set("jmp_customer_jid-#{customer.customer_id}", new_jid)
137 }.then {
138 SwapDefaultFwd.new.do(self, customer, new_jid)
139 }.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