1# frozen_string_literal: true
2
3require "erb"
4
5require_relative "./oob"
6
7class Registration
8 def self.for(iq, customer, web_register_manager)
9 customer.registered?.then do |registered|
10 if registered
11 Registered.new(iq, registered.phone)
12 else
13 web_register_manager.choose_tel(iq).then do |(riq, tel)|
14 Activation.for(riq, customer, tel)
15 end
16 end
17 end
18 end
19
20 class Registered
21 def initialize(iq, tel)
22 @reply = iq.reply
23 @reply.status = :completed
24 @tel = tel
25 end
26
27 def write
28 @reply.note_type = :error
29 @reply.note_text = <<~NOTE
30 You are already registered with JMP number #{@tel}
31 NOTE
32 BLATHER << @reply
33 nil
34 end
35 end
36
37 class Activation
38 def self.for(iq, customer, tel)
39 if customer.active?
40 Finish.new(iq, customer, tel)
41 else
42 EMPromise.resolve(new(iq, customer, tel))
43 end
44 end
45
46 def initialize(iq, customer, tel)
47 @reply = iq.reply
48 reply.allowed_actions = [:next]
49
50 @customer = customer
51 @tel = tel
52 end
53
54 attr_reader :reply, :customer, :tel
55
56 FORM_FIELDS = [
57 {
58 var: "activation_method",
59 type: "list-single",
60 label: "Activate using",
61 required: true,
62 options: [
63 {
64 value: "bitcoin",
65 label: "Bitcoin"
66 },
67 {
68 value: "credit_card",
69 label: "Credit Card ($#{CONFIG[:activation_amount]})"
70 },
71 {
72 value: "code",
73 label: "Invite Code"
74 }
75 ]
76 },
77 {
78 var: "plan_name",
79 type: "list-single",
80 label: "What currency should your account balance be in?",
81 required: true,
82 options: [
83 {
84 value: "cad_beta_unlimited-v20210223",
85 label: "Canadian Dollars"
86 },
87 {
88 value: "usd_beta_unlimited-v20210223",
89 label: "United States Dollars"
90 }
91 ]
92 }
93 ].freeze
94
95 def write
96 rate_center.then do |center|
97 form = reply.form
98 form.type = :form
99 form.title = "Activate JMP"
100 form.instructions = "Going to activate #{tel} (#{center})"
101 form.fields = FORM_FIELDS
102
103 COMMAND_MANAGER.write(reply).then { |iq|
104 Payment.for(iq, customer, tel)
105 }.then(&:write)
106 end
107 end
108
109 protected
110
111 def rate_center
112 EM.promise_fiber do
113 center = BandwidthIris::Tn.get(tel).get_rate_center
114 "#{center[:rate_center]}, #{center[:state]}"
115 end
116 end
117 end
118
119 module Payment
120 def self.kinds
121 @kinds ||= {}
122 end
123
124 def self.for(iq, customer, tel)
125 plan_name = iq.form.field("plan_name").value.to_s
126 customer = customer.with_plan(plan_name)
127 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
128 raise "Invalid activation method"
129 }.call(iq, customer, tel)
130 end
131
132 class Bitcoin
133 Payment.kinds[:bitcoin] = method(:new)
134
135 def initialize(iq, customer, tel)
136 @reply = iq.reply
137 reply.note_type = :info
138 reply.status = :completed
139
140 @customer = customer
141 @customer_id = customer.customer_id
142 @tel = tel
143 @addr = ELECTRUM.createnewaddress
144 end
145
146 attr_reader :reply, :customer_id, :tel
147
148 def save
149 EMPromise.all([
150 REDIS.mset(
151 "pending_tel_for-#{customer_id}", tel,
152 "pending_plan_for-#{customer_id}", @customer.plan_name
153 ),
154 @addr.then do |addr|
155 REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
156 end
157 ])
158 end
159
160 def note_text(amount, addr)
161 <<~NOTE
162 Activate your account by sending at least #{'%.6f' % amount} BTC to
163 #{addr}
164
165 You will receive a notification when your payment is complete.
166 NOTE
167 end
168
169 def write
170 EMPromise.all([
171 @addr,
172 save,
173 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
174 ]).then do |(addr, _, rate)|
175 min = CONFIG[:activation_amount] / rate
176 reply.note_text = note_text(min, addr)
177 BLATHER << reply
178 nil
179 end
180 end
181 end
182
183 class CreditCard
184 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
185
186 def self.for(iq, customer, tel)
187 customer.payment_methods.then do |payment_methods|
188 if (method = payment_methods.default_payment_method)
189 Activate.new(iq, customer, method, tel)
190 else
191 new(iq, customer, tel)
192 end
193 end
194 end
195
196 def initialize(iq, customer, tel)
197 @customer = customer
198 @tel = tel
199
200 @reply = iq.reply
201 @reply.allowed_actions = [:next]
202 @reply.note_type = :info
203 @reply.note_text = "#{oob.desc}: #{oob.url}"
204 end
205
206 attr_reader :reply
207
208 def oob
209 oob = OOB.find_or_create(@reply.command)
210 oob.url = CONFIG[:credit_card_url].call(
211 @reply.to.stripped.to_s,
212 @customer.customer_id
213 )
214 oob.desc = "Add credit card, then return here and choose next"
215 oob
216 end
217
218 def write
219 COMMAND_MANAGER.write(@reply).then do |riq|
220 CreditCard.for(riq, @customer, @tel).write
221 end
222 end
223
224 class Activate
225 def initialize(iq, customer, payment_method, tel)
226 @iq = iq
227 @customer = customer
228 @payment_method = payment_method
229 @tel = tel
230 end
231
232 def write
233 Transaction.sale(
234 @customer,
235 CONFIG[:activation_amount],
236 @payment_method
237 ).then(
238 method(:sold),
239 ->(_) { declined }
240 )
241 end
242
243 protected
244
245 def sold(tx)
246 tx.insert.then {
247 @customer.bill_plan
248 }.then do
249 Finish.new(@iq, @customer, @tel).write
250 end
251 end
252
253 DECLINE_MESSAGE =
254 "Your bank declined the transaction. " \
255 "Often this happens when a person's credit card " \
256 "is a US card that does not support international " \
257 "transactions, as JMP is not based in the USA, though " \
258 "we do support transactions in USD.\n\n" \
259 "If you were trying a prepaid card, you may wish to use "\
260 "Privacy.com instead, as they do support international " \
261 "transactions.\n\n " \
262 "You may add another card and then choose next"
263
264 def decline_oob(reply)
265 oob = OOB.find_or_create(reply.command)
266 oob.url = CONFIG[:credit_card_url].call(
267 reply.to.stripped.to_s,
268 @customer.customer_id
269 )
270 oob.desc = DECLINE_MESSAGE
271 oob
272 end
273
274 def declined
275 reply = @iq.reply
276 reply_oob = decline_oob(reply)
277 reply.allowed_actions = [:next]
278 reply.note_type = :error
279 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
280 COMMAND_MANAGER.write(reply).then do |riq|
281 CreditCard.for(riq, @customer, @tel).write
282 end
283 end
284 end
285 end
286
287 class InviteCode
288 Payment.kinds[:code] = method(:new)
289
290 class Invalid < StandardError; end
291
292 FIELDS = [{
293 var: "code",
294 type: "text-single",
295 label: "Your invite code",
296 required: true
297 }].freeze
298
299 def initialize(iq, customer, tel, error: nil)
300 @customer = customer
301 @tel = tel
302 @reply = iq.reply
303 @reply.allowed_actions = [:next]
304 @form = @reply.form
305 @form.type = :form
306 @form.title = "Enter Invite Code"
307 @form.instructions = error
308 @form.fields = FIELDS
309 end
310
311 def write
312 COMMAND_MANAGER.write(@reply).then do |iq|
313 guard_too_many_tries.then {
314 verify(iq.form.field("code")&.value&.to_s)
315 }.then {
316 Finish.new(iq, @customer, @tel)
317 }.catch_only(Invalid) { |e|
318 invalid_code(iq, e)
319 }.then(&:write)
320 end
321 end
322
323 protected
324
325 def guard_too_many_tries
326 REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
327 raise Invalid, "Too many wrong attempts" if t > 10
328 end
329 end
330
331 def invalid_code(iq, e)
332 EMPromise.all([
333 REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
334 REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
335 end,
336 InviteCode.new(iq, @customer, @tel, error: e.message)
337 ]).then(&:last)
338 end
339
340 def customer_id
341 @customer.customer_id
342 end
343
344 def verify(code)
345 EM.promise_fiber do
346 DB.transaction do
347 valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
348 UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
349 WHERE code=$2 AND used_by_id IS NULL
350 SQL
351 raise Invalid, "Not a valid invite code: #{code}" unless valid
352 @customer.activate_plan_starting_now
353 end
354 end
355 end
356 end
357 end
358
359 class Finish
360 def initialize(iq, customer, tel)
361 @reply = iq.reply
362 @reply.status = :completed
363 @reply.note_type = :info
364 @reply.note_text = "Your JMP account has been activated as #{tel}"
365 @customer = customer
366 @tel = tel
367 end
368
369 def write
370 BandwidthTNOrder.create(@tel).then(&:poll).then(
371 ->(_) { customer_active_tel_purchased },
372 lambda do |_|
373 @reply.note_type = :error
374 @reply.note_text =
375 "The JMP number #{@tel} is no longer available, " \
376 "please visit https://jmp.chat and choose another."
377 BLATHER << @reply
378 end
379 )
380 end
381
382 protected
383
384 def cheogram_sip_addr
385 "sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
386 end
387
388 def customer_active_tel_purchased
389 @customer.register!(@tel).then {
390 EMPromise.all([
391 REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
392 REDIS.set(
393 "catapult_fwd_timeout-#{@reply.to.stripped}",
394 25 # ~5 seconds / ring, 5 rings
395 )
396 ])
397 }.then { BLATHER << @reply }
398 end
399 end
400end