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