1# frozen_string_literal: true
2
3require "erb"
4
5require_relative "./oob"
6
7class Registration
8 def self.for(iq, customer, web_register_manager)
9 customer.registered?.then do |registered|
10 if registered
11 Registered.new(iq, registered.phone)
12 else
13 web_register_manager.choose_tel(iq).then do |(riq, tel)|
14 Activation.for(riq, customer, tel)
15 end
16 end
17 end
18 end
19
20 class Registered
21 def initialize(iq, tel)
22 @reply = iq.reply
23 @reply.status = :completed
24 @tel = tel
25 end
26
27 def write
28 @reply.note_type = :error
29 @reply.note_text = <<~NOTE
30 You are already registered with JMP number #{@tel}
31 NOTE
32 BLATHER << @reply
33 nil
34 end
35 end
36
37 class Activation
38 def self.for(iq, customer, tel)
39 if customer.active?
40 Finish.new(iq, customer, tel)
41 else
42 EMPromise.resolve(new(iq, customer, tel))
43 end
44 end
45
46 def initialize(iq, customer, tel)
47 @reply = iq.reply
48 @reply.status = :executing
49 @reply.allowed_actions = [:next]
50
51 @customer = customer
52 @tel = tel
53 end
54
55 attr_reader :reply, :customer, :tel
56
57 FORM_FIELDS = [
58 {
59 var: "activation_method",
60 type: "list-single",
61 label: "Activate using",
62 required: true,
63 options: [
64 {
65 value: "bitcoin",
66 label: "Bitcoin"
67 },
68 {
69 value: "credit_card",
70 label: "Credit Card"
71 },
72 {
73 value: "code",
74 label: "Invite Code"
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 [
110 "You've selected #{tel} (#{center}) as your JMP number",
111 ACTIVATE_INSTRUCTION,
112 CRYPTOCURRENCY_INSTRUCTION
113 ].each do |txt|
114 form << Blather::XMPPNode.new(:instructions, form.document).tap do |i|
115 i << txt
116 end
117 end
118 end
119
120 def write
121 rate_center.then do |center|
122 form = reply.form
123 form.type = :form
124 form.title = "Activate JMP"
125 add_instructions(form, center)
126 form.fields = FORM_FIELDS
127
128 COMMAND_MANAGER.write(reply).then { |iq|
129 Payment.for(iq, customer, tel)
130 }.then(&:write)
131 end
132 end
133
134 protected
135
136 def rate_center
137 EM.promise_fiber do
138 center = BandwidthIris::Tn.get(tel).get_rate_center
139 "#{center[:rate_center]}, #{center[:state]}"
140 end
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(iq, customer, tel)
155 end
156
157 class Bitcoin
158 Payment.kinds[:bitcoin] = method(:new)
159
160 def initialize(iq, customer, tel)
161 @reply = iq.reply
162 reply.note_type = :info
163 reply.status = :completed
164
165 @customer = customer
166 @customer_id = customer.customer_id
167 @tel = tel
168 @addr = ELECTRUM.createnewaddress
169 end
170
171 attr_reader :reply, :customer_id, :tel
172
173 def save
174 EMPromise.all([
175 REDIS.mset(
176 "pending_tel_for-#{customer_id}", tel,
177 "pending_plan_for-#{customer_id}", @customer.plan_name
178 ),
179 @addr.then do |addr|
180 REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
181 end
182 ])
183 end
184
185 def note_text(amount, addr)
186 <<~NOTE
187 Activate your account by sending at least #{'%.6f' % amount} BTC to
188 #{addr}
189
190 You will receive a notification when your payment is complete.
191 NOTE
192 end
193
194 def write
195 EMPromise.all([
196 @addr,
197 save,
198 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
199 ]).then do |(addr, _, rate)|
200 min = CONFIG[:activation_amount] / rate
201 reply.note_text = note_text(min, addr)
202 BLATHER << reply
203 nil
204 end
205 end
206 end
207
208 class CreditCard
209 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
210
211 def self.for(iq, customer, tel)
212 customer.payment_methods.then do |payment_methods|
213 if (method = payment_methods.default_payment_method)
214 Activate.new(iq, customer, method, tel)
215 else
216 new(iq, customer, tel)
217 end
218 end
219 end
220
221 def initialize(iq, customer, tel)
222 @customer = customer
223 @tel = tel
224
225 @reply = iq.reply
226 @reply.status = :executing
227 @reply.allowed_actions = [:next]
228 @reply.note_type = :info
229 @reply.note_text = "#{oob.desc}: #{oob.url}"
230 end
231
232 attr_reader :reply
233
234 def oob
235 oob = OOB.find_or_create(@reply.command)
236 oob.url = CONFIG[:credit_card_url].call(
237 reply.to.stripped.to_s.gsub("\\", "%5C"),
238 @customer.customer_id
239 )
240 oob.desc = "Add credit card, then return here and choose next"
241 oob
242 end
243
244 def write
245 COMMAND_MANAGER.write(@reply).then do |riq|
246 CreditCard.for(riq, @customer, @tel).write
247 end
248 end
249
250 class Activate
251 def initialize(iq, customer, payment_method, tel)
252 @iq = iq
253 @customer = customer
254 @payment_method = payment_method
255 @tel = tel
256 end
257
258 def write
259 Transaction.sale(
260 @customer,
261 CONFIG[:activation_amount],
262 @payment_method
263 ).then(
264 method(:sold),
265 ->(_) { declined }
266 )
267 end
268
269 protected
270
271 def sold(tx)
272 tx.insert.then {
273 @customer.bill_plan
274 }.then do
275 Finish.new(@iq, @customer, @tel).write
276 end
277 end
278
279 DECLINE_MESSAGE =
280 "Your bank declined the transaction. " \
281 "Often this happens when a person's credit card " \
282 "is a US card that does not support international " \
283 "transactions, as JMP is not based in the USA, though " \
284 "we do support transactions in USD.\n\n" \
285 "If you were trying a prepaid card, you may wish to use "\
286 "Privacy.com instead, as they do support international " \
287 "transactions.\n\n " \
288 "You may add another card and then choose next"
289
290 def decline_oob(reply)
291 oob = OOB.find_or_create(reply.command)
292 oob.url = CONFIG[:credit_card_url].call(
293 reply.to.stripped.to_s.gsub("\\", "%5C"),
294 @customer.customer_id
295 )
296 oob.desc = DECLINE_MESSAGE
297 oob
298 end
299
300 def declined
301 reply = @iq.reply
302 reply_oob = decline_oob(reply)
303 reply.status = :executing
304 reply.allowed_actions = [:next]
305 reply.note_type = :error
306 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
307 COMMAND_MANAGER.write(reply).then do |riq|
308 CreditCard.for(riq, @customer, @tel).write
309 end
310 end
311 end
312 end
313
314 class InviteCode
315 Payment.kinds[:code] = method(:new)
316
317 class Invalid < StandardError; end
318
319 FIELDS = [{
320 var: "code",
321 type: "text-single",
322 label: "Your invite code",
323 required: true
324 }].freeze
325
326 def initialize(iq, customer, tel, error: nil)
327 @customer = customer
328 @tel = tel
329 @reply = iq.reply
330 @reply.status = :executing
331 @reply.allowed_actions = [:next]
332 @form = @reply.form
333 @form.type = :form
334 @form.title = "Enter Invite Code"
335 @form.instructions = error
336 @form.fields = FIELDS
337 end
338
339 def write
340 COMMAND_MANAGER.write(@reply).then do |iq|
341 guard_too_many_tries.then {
342 verify(iq.form.field("code")&.value&.to_s)
343 }.then {
344 Finish.new(iq, @customer, @tel)
345 }.catch_only(Invalid) { |e|
346 invalid_code(iq, e)
347 }.then(&:write)
348 end
349 end
350
351 protected
352
353 def guard_too_many_tries
354 REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
355 raise Invalid, "Too many wrong attempts" if t > 10
356 end
357 end
358
359 def invalid_code(iq, e)
360 EMPromise.all([
361 REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
362 REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
363 end,
364 InviteCode.new(iq, @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 EM.promise_fiber 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 end
386
387 class Finish
388 def initialize(iq, customer, tel)
389 @reply = iq.reply
390 @reply.status = :completed
391 @reply.note_type = :info
392 @reply.note_text = "Your JMP account has been activated as #{tel}"
393 @customer = customer
394 @tel = tel
395 end
396
397 def write
398 BandwidthTNOrder.create(@tel).then(&:poll).then(
399 ->(_) { customer_active_tel_purchased },
400 lambda do |_|
401 @reply.note_type = :error
402 @reply.note_text =
403 "The JMP number #{@tel} is no longer available, " \
404 "please visit https://jmp.chat and choose another."
405 BLATHER << @reply
406 end
407 )
408 end
409
410 protected
411
412 def cheogram_sip_addr
413 "sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
414 end
415
416 def customer_active_tel_purchased
417 @customer.register!(@tel).then {
418 EMPromise.all([
419 REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
420 REDIS.set(
421 "catapult_fwd_timeout-#{@reply.to.stripped}",
422 25 # ~5 seconds / ring, 5 rings
423 )
424 ])
425 }.then { BLATHER << @reply }
426 end
427 end
428end