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