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