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