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 "./tel_selections"
13
14class Registration
15 def self.for(customer, tel_selections)
16 customer.registered?.then do |registered|
17 if registered
18 Registered.new(registered.phone)
19 else
20 tel_selections[customer.jid].then(&: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, final_message: nil, finish: Finish)
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, final_message: final_message, finish: finish)
155 end
156
157 class Bitcoin
158 Payment.kinds[:bitcoin] = method(:new)
159
160 THIRTY_DAYS = 60 * 60 * 24 * 30
161
162 def initialize(customer, tel, final_message: nil, **)
163 @customer = customer
164 @customer_id = customer.customer_id
165 @tel = tel
166 @final_message = final_message
167 end
168
169 attr_reader :customer_id, :tel
170
171 def save
172 EMPromise.all([
173 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel),
174 REDIS.setex(
175 "pending_plan_for-#{customer_id}",
176 THIRTY_DAYS,
177 @customer.plan_name
178 )
179 ])
180 end
181
182 def note_text(amount, addr)
183 <<~NOTE
184 Activate your account by sending at least #{'%.6f' % amount} BTC to
185 #{addr}
186
187 You will receive a notification when your payment is complete.
188 NOTE
189 end
190
191 def write
192 EMPromise.all([
193 addr,
194 save,
195 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
196 ]).then do |(addr, _, rate)|
197 min = CONFIG[:activation_amount] / rate
198 Command.finish(
199 note_text(min, addr) + @final_message.to_s, status: :canceled
200 )
201 end
202 end
203
204 protected
205
206 def addr
207 @addr ||= @customer.btc_addresses.then do |addrs|
208 addrs.first || @customer.add_btc_address
209 end
210 end
211 end
212
213 class CreditCard
214 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
215
216 def self.for(customer, tel, finish: Finish, **)
217 customer.payment_methods.then do |payment_methods|
218 if (method = payment_methods.default_payment_method)
219 Activate.new(customer, method, tel, finish: finish)
220 else
221 new(customer, tel, finish: finish)
222 end
223 end
224 end
225
226 def initialize(customer, tel, finish: Finish)
227 @customer = customer
228 @tel = tel
229 @finish = finish
230 end
231
232 def oob(reply)
233 oob = OOB.find_or_create(reply.command)
234 oob.url = CONFIG[:credit_card_url].call(
235 reply.to.stripped.to_s.gsub("\\", "%5C"),
236 @customer.customer_id
237 )
238 oob.desc = "Add credit card, then return here to continue"
239 oob
240 end
241
242 def write
243 Command.reply { |reply|
244 reply.allowed_actions = [:next]
245 reply.note_type = :info
246 reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}"
247 }.then do
248 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
249 end
250 end
251
252 class Activate
253 def initialize(customer, payment_method, tel, finish: Finish)
254 @customer = customer
255 @payment_method = payment_method
256 @tel = tel
257 @finish = finish
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, finish: @finish).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, final_message: nil, **)
395 @final_message = final_message
396 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 "#{@final_message}"
407
408 form.fields = fields.to_a
409 form
410 end
411
412 def fields
413 [
414 AltTopUpForm::MAILING_ADDRESS,
415 AltTopUpForm::IS_CAD
416 ].flatten
417 end
418
419 def write
420 Command.finish(status: :canceled) do |reply|
421 reply.command << form
422 end
423 end
424 end
425 end
426
427 class Finish
428 def initialize(customer, tel)
429 @customer = customer
430 @tel = tel
431 end
432
433 def write
434 BandwidthTNOrder.create(@tel).then(&:poll).then(
435 ->(_) { customer_active_tel_purchased },
436 ->(_) { number_purchase_error }
437 )
438 end
439
440 protected
441
442 def number_purchase_error
443 TEL_SELECTIONS.delete(@customer.jid).then {
444 TelSelections::ChooseTel.new.choose_tel(
445 error: "The JMP number #{@tel} is no longer available."
446 )
447 }.then { |tel| Finish.new(@customer, tel).write }
448 end
449
450 def cheogram_sip_addr
451 "sip:#{ERB::Util.url_encode(@customer.jid)}@sip.cheogram.com"
452 end
453
454 def raise_setup_error(e)
455 Command.log.error "@customer.register! failed", e
456 Command.finish(
457 "There was an error setting up your number, " \
458 "please contact JMP support.",
459 type: :error
460 )
461 end
462
463 def customer_active_tel_purchased
464 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
465 EMPromise.all([
466 REDIS.del("pending_tel_for-#{@customer.jid}"),
467 REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
468 @customer.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
469 ])
470 }.then do
471 Command.finish("Your JMP account has been activated as #{@tel}")
472 end
473 end
474 end
475end