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 [
120 "You've selected #{tel} (#{center}) as your JMP number",
121 ACTIVATE_INSTRUCTION,
122 CRYPTOCURRENCY_INSTRUCTION
123 ].each do |txt|
124 form << Blather::XMPPNode.new(:instructions, form.document).tap do |i|
125 i << txt
126 end
127 end
128 end
129
130 def write
131 rate_center.then do |center|
132 form = reply.form
133 form.type = :form
134 form.title = "Activate JMP"
135 add_instructions(form, center)
136 form.fields = FORM_FIELDS
137
138 COMMAND_MANAGER.write(reply).then { |iq|
139 Payment.for(iq, customer, tel)
140 }.then(&:write)
141 end
142 end
143
144 protected
145
146 def rate_center
147 EM.promise_fiber do
148 center = BandwidthIris::Tn.get(tel).get_rate_center
149 "#{center[:rate_center]}, #{center[:state]}"
150 end
151 end
152 end
153
154 module Payment
155 def self.kinds
156 @kinds ||= {}
157 end
158
159 def self.for(iq, customer, tel)
160 plan_name = iq.form.field("plan_name").value.to_s
161 customer = customer.with_plan(plan_name)
162 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
163 raise "Invalid activation method"
164 }.call(iq, customer, tel)
165 end
166
167 class Bitcoin
168 Payment.kinds[:bitcoin] = method(:new)
169
170 def initialize(iq, customer, tel)
171 @reply = iq.reply
172 reply.note_type = :info
173 reply.status = :completed
174
175 @customer = customer
176 @customer_id = customer.customer_id
177 @tel = tel
178 end
179
180 attr_reader :reply, :customer_id, :tel
181
182 def legacy_session_save
183 sid = SecureRandom.hex
184 REDIS.mset(
185 "reg-sid_for-#{customer_id}", sid,
186 "reg-session_tel-#{sid}", tel
187 )
188 end
189
190 def save
191 EMPromise.all([
192 legacy_session_save,
193 REDIS.mset(
194 "pending_tel_for-#{customer_id}", tel,
195 "pending_plan_for-#{customer_id}", @customer.plan_name
196 )
197 ])
198 end
199
200 def note_text(amount, addr)
201 <<~NOTE
202 Activate your account by sending at least #{'%.6f' % amount} BTC to
203 #{addr}
204
205 You will receive a notification when your payment is complete.
206 NOTE
207 end
208
209 def write
210 EMPromise.all([
211 addr,
212 save,
213 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
214 ]).then do |(addr, _, rate)|
215 min = CONFIG[:activation_amount] / rate
216 reply.note_text = note_text(min, addr)
217 BLATHER << reply
218 nil
219 end
220 end
221
222 protected
223
224 def addr
225 @addr ||= @customer.btc_addresses.then do |addrs|
226 addrs.first || @customer.add_btc_address
227 end
228 end
229 end
230
231 class CreditCard
232 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
233
234 def self.for(iq, customer, tel)
235 customer.payment_methods.then do |payment_methods|
236 if (method = payment_methods.default_payment_method)
237 Activate.new(iq, customer, method, tel)
238 else
239 new(iq, customer, tel)
240 end
241 end
242 end
243
244 def initialize(iq, customer, tel)
245 @customer = customer
246 @tel = tel
247
248 @reply = iq.reply
249 @reply.status = :executing
250 @reply.allowed_actions = [:next]
251 @reply.note_type = :info
252 @reply.note_text = "#{oob.desc}: #{oob.url}"
253 end
254
255 attr_reader :reply
256
257 def oob
258 oob = OOB.find_or_create(@reply.command)
259 oob.url = CONFIG[:credit_card_url].call(
260 reply.to.stripped.to_s.gsub("\\", "%5C"),
261 @customer.customer_id
262 )
263 oob.desc = "Add credit card, then return here and choose next"
264 oob
265 end
266
267 def write
268 COMMAND_MANAGER.write(@reply).then do |riq|
269 CreditCard.for(riq, @customer, @tel).write
270 end
271 end
272
273 class Activate
274 def initialize(iq, customer, payment_method, tel)
275 @iq = iq
276 @customer = customer
277 @payment_method = payment_method
278 @tel = tel
279 end
280
281 def write
282 Transaction.sale(
283 @customer,
284 amount: CONFIG[:activation_amount],
285 payment_method: @payment_method
286 ).then(
287 method(:sold),
288 ->(_) { declined }
289 )
290 end
291
292 protected
293
294 def sold(tx)
295 tx.insert.then {
296 @customer.bill_plan
297 }.then do
298 Finish.new(@iq, @customer, @tel).write
299 end
300 end
301
302 DECLINE_MESSAGE =
303 "Your bank declined the transaction. " \
304 "Often this happens when a person's credit card " \
305 "is a US card that does not support international " \
306 "transactions, as JMP is not based in the USA, though " \
307 "we do support transactions in USD.\n\n" \
308 "If you were trying a prepaid card, you may wish to use "\
309 "Privacy.com instead, as they do support international " \
310 "transactions.\n\n " \
311 "You may add another card and then choose next"
312
313 def decline_oob(reply)
314 oob = OOB.find_or_create(reply.command)
315 oob.url = CONFIG[:credit_card_url].call(
316 reply.to.stripped.to_s.gsub("\\", "%5C"),
317 @customer.customer_id
318 )
319 oob.desc = DECLINE_MESSAGE
320 oob
321 end
322
323 def declined
324 reply = @iq.reply
325 reply_oob = decline_oob(reply)
326 reply.status = :executing
327 reply.allowed_actions = [:next]
328 reply.note_type = :error
329 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
330 COMMAND_MANAGER.write(reply).then do |riq|
331 CreditCard.for(riq, @customer, @tel).write
332 end
333 end
334 end
335 end
336
337 class InviteCode
338 Payment.kinds[:code] = method(:new)
339
340 class Invalid < StandardError; end
341
342 FIELDS = [{
343 var: "code",
344 type: "text-single",
345 label: "Your invite code",
346 required: true
347 }].freeze
348
349 def initialize(iq, customer, tel, error: nil)
350 @customer = customer
351 @tel = tel
352 @reply = iq.reply
353 @reply.status = :executing
354 @reply.allowed_actions = [:next]
355 @form = @reply.form
356 @form.type = :form
357 @form.title = "Enter Invite Code"
358 @form.instructions = error
359 @form.fields = FIELDS
360 end
361
362 def write
363 COMMAND_MANAGER.write(@reply).then do |iq|
364 guard_too_many_tries.then {
365 verify(iq.form.field("code")&.value&.to_s)
366 }.then {
367 Finish.new(iq, @customer, @tel)
368 }.catch_only(Invalid) { |e|
369 invalid_code(iq, e)
370 }.then(&:write)
371 end
372 end
373
374 protected
375
376 def guard_too_many_tries
377 REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
378 raise Invalid, "Too many wrong attempts" if t > 10
379 end
380 end
381
382 def invalid_code(iq, e)
383 EMPromise.all([
384 REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
385 REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
386 end,
387 InviteCode.new(iq, @customer, @tel, error: e.message)
388 ]).then(&:last)
389 end
390
391 def customer_id
392 @customer.customer_id
393 end
394
395 def verify(code)
396 EM.promise_fiber do
397 DB.transaction do
398 valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
399 UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
400 WHERE code=$2 AND used_by_id IS NULL
401 SQL
402 raise Invalid, "Not a valid invite code: #{code}" unless valid
403 @customer.activate_plan_starting_now
404 end
405 end
406 end
407 end
408
409 class Mail
410 Payment.kinds[:mail] = method(:new)
411
412 def initialize(iq, _customer, _tel)
413 @reply = iq.reply
414 @reply.status = :completed
415 end
416
417 def form
418 form = Blather::Stanza::X.new(:result)
419 form.title = "Activate by Mail or eTransfer"
420 form.instructions =
421 "We support payment by postal mail or, in Canada by Interac, " \
422 "eTransfer. Minimum deposit for a new account is " \
423 "$#{CONFIG[:activation_amount]}"
424
425 form.fields = fields.to_a
426 form
427 end
428
429 def fields
430 [
431 AltTopUpForm::MAILING_ADDRESS,
432 AltTopUpForm::IS_CAD
433 ].flatten
434 end
435
436 def write
437 @reply.command << form
438 BLATHER << @reply
439 end
440 end
441 end
442
443 class Finish
444 def initialize(iq, customer, tel)
445 @reply = iq.reply
446 @reply.status = :completed
447 @reply.note_type = :info
448 @reply.note_text = "Your JMP account has been activated as #{tel}"
449 @customer = customer
450 @tel = tel
451 end
452
453 def write
454 BandwidthTNOrder.create(@tel).then(&:poll).then(
455 ->(_) { customer_active_tel_purchased },
456 lambda do |_|
457 @reply.note_type = :error
458 @reply.note_text =
459 "The JMP number #{@tel} is no longer available, " \
460 "please visit https://jmp.chat and choose another."
461 BLATHER << @reply
462 end
463 )
464 end
465
466 protected
467
468 def cheogram_sip_addr
469 "sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
470 end
471
472 def customer_active_tel_purchased
473 @customer.register!(@tel).then {
474 EMPromise.all([
475 REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
476 REDIS.set(
477 "catapult_fwd_timeout-#{@reply.to.stripped}",
478 25 # ~5 seconds / ring, 5 rings
479 )
480 ])
481 }.then { BLATHER << @reply }
482 end
483 end
484end