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