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