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