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