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