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