registration.rb

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