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 { |i|
114 i << txt
115 }
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 { |addrs|
206 addrs.first || @customer.add_btc_address
207 }
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
384 @customer.activate_plan_starting_now
385 end
386 end
387 end
388 end
389
390 class Mail
391 Payment.kinds[:mail] = method(:new)
392
393 def initialize(_customer, _tel, final_message: nil, **)
394 @final_message = final_message
395 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 "#{@final_message}"
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 ->(_) { number_purchase_error }
436 )
437 end
438
439 protected
440
441 def number_purchase_error
442 TEL_SELECTIONS.delete(@customer.jid).then {
443 TelSelections::ChooseTel.new.choose_tel(
444 error: "The JMP number #{@tel} is no longer available."
445 )
446 }.then { |tel| Finish.new(@customer, tel).write }
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.del("pending_tel_for-#{@customer.jid}"),
462 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
463 uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
464 ))
465 ])
466 }.then do
467 Command.finish("Your JMP account has been activated as #{@tel}")
468 end
469 end
470 end
471end