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