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 ($#{CONFIG[:activation_amount]})"
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 def write
97 rate_center.then do |center|
98 form = reply.form
99 form.type = :form
100 form.title = "Activate JMP"
101 form.instructions = "Going to activate #{tel} (#{center})"
102 form.fields = FORM_FIELDS
103
104 COMMAND_MANAGER.write(reply).then { |iq|
105 Payment.for(iq, customer, tel)
106 }.then(&:write)
107 end
108 end
109
110 protected
111
112 def rate_center
113 EM.promise_fiber do
114 center = BandwidthIris::Tn.get(tel).get_rate_center
115 "#{center[:rate_center]}, #{center[:state]}"
116 end
117 end
118 end
119
120 module Payment
121 def self.kinds
122 @kinds ||= {}
123 end
124
125 def self.for(iq, customer, tel)
126 plan_name = iq.form.field("plan_name").value.to_s
127 customer = customer.with_plan(plan_name)
128 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
129 raise "Invalid activation method"
130 }.call(iq, customer, tel)
131 end
132
133 class Bitcoin
134 Payment.kinds[:bitcoin] = method(:new)
135
136 def initialize(iq, customer, tel)
137 @reply = iq.reply
138 reply.note_type = :info
139 reply.status = :completed
140
141 @customer = customer
142 @customer_id = customer.customer_id
143 @tel = tel
144 @addr = ELECTRUM.createnewaddress
145 end
146
147 attr_reader :reply, :customer_id, :tel
148
149 def save
150 EMPromise.all([
151 REDIS.mset(
152 "pending_tel_for-#{customer_id}", tel,
153 "pending_plan_for-#{customer_id}", @customer.plan_name
154 ),
155 @addr.then do |addr|
156 REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
157 end
158 ])
159 end
160
161 def note_text(amount, addr)
162 <<~NOTE
163 Activate your account by sending at least #{'%.6f' % amount} BTC to
164 #{addr}
165
166 You will receive a notification when your payment is complete.
167 NOTE
168 end
169
170 def write
171 EMPromise.all([
172 @addr,
173 save,
174 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
175 ]).then do |(addr, _, rate)|
176 min = CONFIG[:activation_amount] / rate
177 reply.note_text = note_text(min, addr)
178 BLATHER << reply
179 nil
180 end
181 end
182 end
183
184 class CreditCard
185 Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
186
187 def self.for(iq, customer, tel)
188 customer.payment_methods.then do |payment_methods|
189 if (method = payment_methods.default_payment_method)
190 Activate.new(iq, customer, method, tel)
191 else
192 new(iq, customer, tel)
193 end
194 end
195 end
196
197 def initialize(iq, customer, tel)
198 @customer = customer
199 @tel = tel
200
201 @reply = iq.reply
202 @reply.status = :executing
203 @reply.allowed_actions = [:next]
204 @reply.note_type = :info
205 @reply.note_text = "#{oob.desc}: #{oob.url}"
206 end
207
208 attr_reader :reply
209
210 def oob
211 oob = OOB.find_or_create(@reply.command)
212 oob.url = CONFIG[:credit_card_url].call(
213 reply.to.stripped.to_s.gsub("\\", "%5C"),
214 @customer.customer_id
215 )
216 oob.desc = "Add credit card, then return here and choose next"
217 oob
218 end
219
220 def write
221 COMMAND_MANAGER.write(@reply).then do |riq|
222 CreditCard.for(riq, @customer, @tel).write
223 end
224 end
225
226 class Activate
227 def initialize(iq, customer, payment_method, tel)
228 @iq = iq
229 @customer = customer
230 @payment_method = payment_method
231 @tel = tel
232 end
233
234 def write
235 Transaction.sale(
236 @customer,
237 CONFIG[:activation_amount],
238 @payment_method
239 ).then(
240 method(:sold),
241 ->(_) { declined }
242 )
243 end
244
245 protected
246
247 def sold(tx)
248 tx.insert.then {
249 @customer.bill_plan
250 }.then do
251 Finish.new(@iq, @customer, @tel).write
252 end
253 end
254
255 DECLINE_MESSAGE =
256 "Your bank declined the transaction. " \
257 "Often this happens when a person's credit card " \
258 "is a US card that does not support international " \
259 "transactions, as JMP is not based in the USA, though " \
260 "we do support transactions in USD.\n\n" \
261 "If you were trying a prepaid card, you may wish to use "\
262 "Privacy.com instead, as they do support international " \
263 "transactions.\n\n " \
264 "You may add another card and then choose next"
265
266 def decline_oob(reply)
267 oob = OOB.find_or_create(reply.command)
268 oob.url = CONFIG[:credit_card_url].call(
269 reply.to.stripped.to_s.gsub("\\", "%5C"),
270 @customer.customer_id
271 )
272 oob.desc = DECLINE_MESSAGE
273 oob
274 end
275
276 def declined
277 reply = @iq.reply
278 reply_oob = decline_oob(reply)
279 reply.status = :executing
280 reply.allowed_actions = [:next]
281 reply.note_type = :error
282 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
283 COMMAND_MANAGER.write(reply).then do |riq|
284 CreditCard.for(riq, @customer, @tel).write
285 end
286 end
287 end
288 end
289
290 class InviteCode
291 Payment.kinds[:code] = method(:new)
292
293 class Invalid < StandardError; end
294
295 FIELDS = [{
296 var: "code",
297 type: "text-single",
298 label: "Your invite code",
299 required: true
300 }].freeze
301
302 def initialize(iq, customer, tel, error: nil)
303 @customer = customer
304 @tel = tel
305 @reply = iq.reply
306 @reply.status = :executing
307 @reply.allowed_actions = [:next]
308 @form = @reply.form
309 @form.type = :form
310 @form.title = "Enter Invite Code"
311 @form.instructions = error
312 @form.fields = FIELDS
313 end
314
315 def write
316 COMMAND_MANAGER.write(@reply).then do |iq|
317 guard_too_many_tries.then {
318 verify(iq.form.field("code")&.value&.to_s)
319 }.then {
320 Finish.new(iq, @customer, @tel)
321 }.catch_only(Invalid) { |e|
322 invalid_code(iq, e)
323 }.then(&:write)
324 end
325 end
326
327 protected
328
329 def guard_too_many_tries
330 REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
331 raise Invalid, "Too many wrong attempts" if t > 10
332 end
333 end
334
335 def invalid_code(iq, e)
336 EMPromise.all([
337 REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
338 REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
339 end,
340 InviteCode.new(iq, @customer, @tel, error: e.message)
341 ]).then(&:last)
342 end
343
344 def customer_id
345 @customer.customer_id
346 end
347
348 def verify(code)
349 EM.promise_fiber do
350 DB.transaction do
351 valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
352 UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
353 WHERE code=$2 AND used_by_id IS NULL
354 SQL
355 raise Invalid, "Not a valid invite code: #{code}" unless valid
356 @customer.activate_plan_starting_now
357 end
358 end
359 end
360 end
361 end
362
363 class Finish
364 def initialize(iq, customer, tel)
365 @reply = iq.reply
366 @reply.status = :completed
367 @reply.note_type = :info
368 @reply.note_text = "Your JMP account has been activated as #{tel}"
369 @customer = customer
370 @tel = tel
371 end
372
373 def write
374 BandwidthTNOrder.create(@tel).then(&:poll).then(
375 ->(_) { customer_active_tel_purchased },
376 lambda do |_|
377 @reply.note_type = :error
378 @reply.note_text =
379 "The JMP number #{@tel} is no longer available, " \
380 "please visit https://jmp.chat and choose another."
381 BLATHER << @reply
382 end
383 )
384 end
385
386 protected
387
388 def cheogram_sip_addr
389 "sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
390 end
391
392 def customer_active_tel_purchased
393 @customer.register!(@tel).then {
394 EMPromise.all([
395 REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
396 REDIS.set(
397 "catapult_fwd_timeout-#{@reply.to.stripped}",
398 25 # ~5 seconds / ring, 5 rings
399 )
400 ])
401 }.then { BLATHER << @reply }
402 end
403 end
404end