1# frozen_string_literal: true
2
3require "erb"
4require "ruby-bandwidth-iris"
5require "securerandom"
6
7require_relative "./alt_top_up_form"
8require_relative "./bandwidth_tn_order"
9require_relative "./command"
10require_relative "./em"
11require_relative "./invites_repo"
12require_relative "./oob"
13require_relative "./proxied_jid"
14require_relative "./tel_selections"
15
16class Registration
17 def self.for(customer, tel_selections)
18 if (reg = customer.registered?)
19 Registered.new(reg.phone)
20 else
21 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
22 FinishOrStartActivation.for(customer, tel)
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 FinishOrStartActivation
38 def self.for(customer, tel)
39 if customer.active?
40 Finish.new(customer, tel)
41 elsif customer.balance >= CONFIG[:activation_amount_accept]
42 BillPlan.new(customer, tel)
43 else
44 new(customer, tel)
45 end
46 end
47
48 def initialize(customer, tel)
49 @customer = customer
50 @tel = tel
51 end
52
53 def write
54 Command.reply { |reply|
55 reply.allowed_actions = [:next]
56 reply.note_type = :info
57 reply.note_text = File.read("#{__dir__}/../fup.txt")
58 }.then { Activation.for(@customer, @tel).write }
59 end
60 end
61
62 class Activation
63 def self.for(customer, tel)
64 jid = ProxiedJID.new(customer.jid).unproxied
65 if CONFIG[:approved_domains].key?(jid.domain.to_sym)
66 Allow.for(customer, tel, jid)
67 else
68 new(customer, tel)
69 end
70 end
71
72 def initialize(customer, tel)
73 @customer = customer
74 @tel = tel
75 end
76
77 attr_reader :customer, :tel
78
79 def form(center)
80 FormTemplate.render(
81 "registration/activate",
82 tel: tel,
83 rate_center: center
84 )
85 end
86
87 def write
88 rate_center.then { |center|
89 Command.reply do |reply|
90 reply.allowed_actions = [:next]
91 reply.command << form(center)
92 end
93 }.then(&method(:next_step))
94 end
95
96 def next_step(iq)
97 EMPromise.resolve(nil).then {
98 Payment.for(iq, customer, tel)
99 }.then(&:write)
100 end
101
102 protected
103
104 def rate_center
105 EM.promise_fiber {
106 center = BandwidthIris::Tn.get(tel).get_rate_center
107 "#{center[:rate_center]}, #{center[:state]}"
108 }.catch { nil }
109 end
110
111 class Allow < Activation
112 def self.for(customer, tel, jid)
113 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
114 new(customer, tel, credit_to)
115 end
116
117 def initialize(customer, tel, credit_to)
118 super(customer, tel)
119 @credit_to = credit_to
120 end
121
122 def form(center)
123 FormTemplate.render(
124 "registration/allow",
125 tel: tel,
126 rate_center: center,
127 domain: customer.jid.domain
128 )
129 end
130
131 def next_step(iq)
132 plan_name = iq.form.field("plan_name").value.to_s
133 @customer = customer.with_plan(plan_name)
134 EMPromise.resolve(nil).then { activate }.then do
135 Finish.new(customer, tel).write
136 end
137 end
138
139 protected
140
141 def activate
142 DB.transaction do
143 if @credit_to
144 DB.exec(<<~SQL, [@credit_to, customer.customer_id])
145 INSERT INTO invites (creator_id, used_by_id, used_at)
146 VALUES ($1, $2, LOCALTIMESTAMP)
147 SQL
148 end
149 @customer.activate_plan_starting_now
150 end
151 end
152 end
153 end
154
155 module Payment
156 def self.kinds
157 @kinds ||= {}
158 end
159
160 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
161 plan_name = iq.form.field("plan_name").value.to_s
162 customer = customer.with_plan(plan_name)
163 customer.save_plan!.then do
164 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
165 raise "Invalid activation method"
166 }.call(customer, tel, final_message: final_message, finish: finish)
167 end
168 end
169
170 class Bitcoin
171 Payment.kinds[:bitcoin] = method(:new)
172
173 THIRTY_DAYS = 60 * 60 * 24 * 30
174
175 def initialize(customer, tel, final_message: nil, **)
176 @customer = customer
177 @customer_id = customer.customer_id
178 @tel = tel
179 @final_message = final_message
180 end
181
182 attr_reader :customer_id, :tel
183
184 def save
185 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
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(
205 note_text(min, addr) + @final_message.to_s, status: :canceled
206 )
207 end
208 end
209
210 protected
211
212 def addr
213 @addr ||= @customer.btc_addresses.then { |addrs|
214 addrs.first || @customer.add_btc_address
215 }
216 end
217 end
218
219 class CreditCard
220 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
221
222 def self.for(customer, tel, finish: Finish, **)
223 customer.payment_methods.then do |payment_methods|
224 if (method = payment_methods.default_payment_method)
225 Activate.new(customer, method, tel, finish: finish)
226 else
227 new(customer, tel, finish: finish)
228 end
229 end
230 end
231
232 def initialize(customer, tel, finish: Finish)
233 @customer = customer
234 @tel = tel
235 @finish = finish
236 end
237
238 def oob(reply)
239 oob = OOB.find_or_create(reply.command)
240 oob.url = CONFIG[:credit_card_url].call(
241 reply.to.stripped.to_s.gsub("\\", "%5C"),
242 @customer.customer_id
243 )
244 oob.desc = "Add credit card, then return here to continue"
245 oob
246 end
247
248 def write
249 Command.reply { |reply|
250 reply.allowed_actions = [:next]
251 reply.note_type = :info
252 reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}"
253 }.then do
254 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
255 end
256 end
257
258 class Activate
259 def initialize(customer, payment_method, tel, finish: Finish)
260 @customer = customer
261 @payment_method = payment_method
262 @tel = tel
263 @finish = finish
264 end
265
266 def write
267 Transaction.sale(
268 @customer,
269 amount: CONFIG[:activation_amount],
270 payment_method: @payment_method
271 ).then(
272 method(:sold),
273 ->(_) { declined }
274 )
275 end
276
277 protected
278
279 def sold(tx)
280 tx.insert.then do
281 BillPlan.new(@customer, @tel, finish: @finish).write
282 end
283 end
284
285 DECLINE_MESSAGE =
286 "Your bank declined the transaction. " \
287 "Often this happens when a person's credit card " \
288 "is a US card that does not support international " \
289 "transactions, as JMP is not based in the USA, though " \
290 "we do support transactions in USD.\n\n" \
291 "If you were trying a prepaid card, you may wish to use "\
292 "Privacy.com instead, as they do support international " \
293 "transactions.\n\n " \
294 "You may add another card and then return here"
295
296 def decline_oob(reply)
297 oob = OOB.find_or_create(reply.command)
298 oob.url = CONFIG[:credit_card_url].call(
299 reply.to.stripped.to_s.gsub("\\", "%5C"),
300 @customer.customer_id
301 )
302 oob.desc = DECLINE_MESSAGE
303 oob
304 end
305
306 def declined
307 Command.reply { |reply|
308 reply_oob = decline_oob(reply)
309 reply.allowed_actions = [:next]
310 reply.note_type = :error
311 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
312 }.then do
313 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
314 end
315 end
316 end
317 end
318
319 class InviteCode
320 Payment.kinds[:code] = method(:new)
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(InvitesRepo::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 InvitesRepo::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 InvitesRepo.new(DB).claim_code(customer_id, code) do
381 @customer.activate_plan_starting_now
382 end
383 end
384 end
385
386 class Mail
387 Payment.kinds[:mail] = method(:new)
388
389 def initialize(_customer, _tel, final_message: nil, **)
390 @final_message = final_message
391 end
392
393 def form
394 form = Blather::Stanza::X.new(:result)
395 form.title = "Activate by Mail or Interac e-Transfer"
396 form.instructions =
397 "Activate your account by sending at least " \
398 "$#{CONFIG[:activation_amount]}\nWe support payment by " \
399 "postal mail or, in Canada, by Interac e-Transfer.\n\n" \
400 "You will receive a notification when your payment is complete." \
401 "#{@final_message}"
402
403 form.fields = fields.to_a
404 form
405 end
406
407 def fields
408 [
409 AltTopUpForm::MAILING_ADDRESS,
410 AltTopUpForm::IS_CAD
411 ].flatten
412 end
413
414 def write
415 Command.finish(status: :canceled) do |reply|
416 reply.command << form
417 end
418 end
419 end
420 end
421
422 class BillPlan
423 def initialize(customer, tel, finish: Finish)
424 @customer = customer
425 @tel = tel
426 @finish = finish
427 end
428
429 def write
430 @customer.bill_plan(note: "Bill for first month").then do
431 @finish.new(@customer, @tel).write
432 end
433 end
434 end
435
436 class Finish
437 def initialize(customer, tel)
438 @customer = customer
439 @tel = tel
440 end
441
442 def write
443 BandwidthTNOrder.create(@tel).then(&:poll).then(
444 ->(_) { customer_active_tel_purchased },
445 ->(_) { number_purchase_error }
446 )
447 end
448
449 protected
450
451 def number_purchase_error
452 TEL_SELECTIONS.delete(@customer.jid).then {
453 TelSelections::ChooseTel.new.choose_tel(
454 error: "The JMP number #{@tel} is no longer available."
455 )
456 }.then { |tel| Finish.new(@customer, tel).write }
457 end
458
459 def raise_setup_error(e)
460 Command.log.error "@customer.register! failed", e
461 Command.finish(
462 "There was an error setting up your number, " \
463 "please contact JMP support.",
464 type: :error
465 )
466 end
467
468 def customer_active_tel_purchased
469 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
470 EMPromise.all([
471 REDIS.del("pending_tel_for-#{@customer.jid}"),
472 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
473 uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
474 ))
475 ])
476 }.then do
477 Command.finish("Your JMP account has been activated as #{@tel}")
478 end
479 end
480 end
481end