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