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