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