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