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