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 jid = ProxiedJID.new(customer.jid).unproxied
38 if customer.active?
39 Finish.new(customer, tel)
40 elsif CONFIG[:approved_domains].key?(jid.domain.to_sym)
41 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
42 Allow.new(customer, tel, credit_to)
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 initialize(customer, tel, credit_to)
89 super(customer, tel)
90 @credit_to = credit_to
91 end
92
93 def form(center)
94 FormTemplate.render(
95 "registration/allow",
96 tel: tel,
97 rate_center: center,
98 domain: customer.jid.domain
99 )
100 end
101
102 def next_step(iq)
103 plan_name = iq.form.field("plan_name").value.to_s
104 @customer = customer.with_plan(plan_name)
105 EMPromise.resolve(nil).then { activate }.then do
106 Finish.new(customer, tel).write
107 end
108 end
109
110 protected
111
112 def activate
113 DB.transaction do
114 if @credit_to
115 DB.exec(<<~SQL, [@credit_to, customer.customer_id])
116 INSERT INTO invites (creator_id, used_by_id, used_at)
117 VALUES ($1, $2, LOCALTIMESTAMP)
118 SQL
119 end
120 @customer.activate_plan_starting_now
121 end
122 end
123 end
124 end
125
126 module Payment
127 def self.kinds
128 @kinds ||= {}
129 end
130
131 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
132 plan_name = iq.form.field("plan_name").value.to_s
133 customer = customer.with_plan(plan_name)
134 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
135 raise "Invalid activation method"
136 }.call(customer, tel, final_message: final_message, finish: finish)
137 end
138
139 class Bitcoin
140 Payment.kinds[:bitcoin] = method(:new)
141
142 THIRTY_DAYS = 60 * 60 * 24 * 30
143
144 def initialize(customer, tel, final_message: nil, **)
145 @customer = customer
146 @customer_id = customer.customer_id
147 @tel = tel
148 @final_message = final_message
149 end
150
151 attr_reader :customer_id, :tel
152
153 def save
154 EMPromise.all([
155 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel),
156 REDIS.setex(
157 "pending_plan_for-#{customer_id}",
158 THIRTY_DAYS,
159 @customer.plan_name
160 )
161 ])
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 {
257 @customer.bill_plan
258 }.then do
259 @finish.new(@customer, @tel).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 Finish
411 def initialize(customer, tel)
412 @customer = customer
413 @tel = tel
414 end
415
416 def write
417 BandwidthTNOrder.create(@tel).then(&:poll).then(
418 ->(_) { customer_active_tel_purchased },
419 ->(_) { number_purchase_error }
420 )
421 end
422
423 protected
424
425 def number_purchase_error
426 TEL_SELECTIONS.delete(@customer.jid).then {
427 TelSelections::ChooseTel.new.choose_tel(
428 error: "The JMP number #{@tel} is no longer available."
429 )
430 }.then { |tel| Finish.new(@customer, tel).write }
431 end
432
433 def raise_setup_error(e)
434 Command.log.error "@customer.register! failed", e
435 Command.finish(
436 "There was an error setting up your number, " \
437 "please contact JMP support.",
438 type: :error
439 )
440 end
441
442 def customer_active_tel_purchased
443 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
444 EMPromise.all([
445 REDIS.del("pending_tel_for-#{@customer.jid}"),
446 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
447 uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
448 ))
449 ])
450 }.then do
451 Command.finish("Your JMP account has been activated as #{@tel}")
452 end
453 end
454 end
455end