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