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 "./bandwidth_tn_order"
  9require_relative "./bandwidth_tn_reservation_repo"
 10require_relative "./command"
 11require_relative "./em"
 12require_relative "./invites_repo"
 13require_relative "./oob"
 14require_relative "./parent_code_repo"
 15require_relative "./proxied_jid"
 16require_relative "./tel_selections"
 17require_relative "./welcome_message"
 18
 19class Registration
 20	def self.for(customer, google_play_userid, tel_selections)
 21		if (reg = customer.registered?)
 22			Registered.for(customer, reg.phone)
 23		else
 24			tel_selections[customer.jid].then(&:choose_tel).then do |tel|
 25				reserve_and_continue(tel_selections, customer, tel).then do
 26					FinishOrStartActivation.for(customer, google_play_userid, tel)
 27				end
 28			end
 29		end
 30	end
 31
 32	def self.reserve_and_continue(tel_selections, customer, tel)
 33		tel.reserve(customer).catch do
 34			tel_selections.delete(customer.jid).then {
 35				tel_selections[customer.jid]
 36			}.then { |choose|
 37				choose.choose_tel(
 38					error: "The JMP number #{tel} is no longer available."
 39				)
 40			}.then { |n_tel| reserve_and_continue(tel_selections, customer, n_tel) }
 41		end
 42	end
 43
 44	class Registered
 45		def self.for(customer, tel)
 46			jid = ProxiedJID.new(customer.jid).unproxied
 47			if jid.domain == CONFIG[:onboarding_domain]
 48				FinishOnboarding.for(customer, tel)
 49			else
 50				new(tel)
 51			end
 52		end
 53
 54		def initialize(tel)
 55			@tel = tel
 56		end
 57
 58		def write
 59			Command.finish("You are already registered with JMP number #{@tel}")
 60		end
 61	end
 62
 63	class FinishOrStartActivation
 64		def self.for(customer, google_play_userid, tel)
 65			if customer.active?
 66				Finish.new(customer, tel)
 67			elsif customer.balance >= CONFIG[:activation_amount_accept]
 68				BillPlan.new(customer, tel)
 69			else
 70				new(customer, google_play_userid, tel)
 71			end
 72		end
 73
 74		def initialize(customer, google_play_userid, tel)
 75			@customer = customer
 76			@tel = tel
 77			@google_play_userid = google_play_userid
 78		end
 79
 80		def write
 81			Command.reply { |reply|
 82				reply.allowed_actions = [:next]
 83				reply.note_type = :info
 84				reply.note_text = File.read("#{__dir__}/../fup.txt")
 85			}.then { Activation.for(@customer, @google_play_userid, @tel).write }
 86		end
 87	end
 88
 89	class Activation
 90		def self.for(customer, google_play_userid, tel)
 91			jid = ProxiedJID.new(customer.jid).unproxied
 92			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
 93				Allow.for(customer, tel, jid)
 94			elsif google_play_userid
 95				GooglePlay.new(customer, google_play_userid, tel)
 96			else
 97				new(customer, tel)
 98			end
 99		end
100
101		def initialize(customer, tel)
102			@customer = customer
103			@tel = tel
104			@invites = InvitesRepo.new(DB, REDIS)
105		end
106
107		attr_reader :customer, :tel
108
109		def form
110			FormTemplate.render("registration/activate", tel: tel)
111		end
112
113		def write
114			Command.reply { |reply|
115				reply.allowed_actions = [:next]
116				reply.command << form
117			}.then(&method(:next_step))
118		end
119
120		def next_step(iq)
121			code = iq.form.field("code")&.value&.to_s
122			save_customer_plan(iq, code).then {
123				finish_if_valid_invite(code)
124			}.catch_only(InvitesRepo::Invalid) do
125				@invites.stash_code(customer.customer_id, code).then do
126					Payment.for(iq, @customer, @tel).then(&:write)
127				end
128			end
129		end
130
131	protected
132
133		def finish_if_valid_invite(code)
134			@invites.claim_code(@customer.customer_id, code) {
135				@customer.activate_plan_starting_now
136			}.then do
137				Finish.new(@customer, @tel).write
138			end
139		end
140
141		def save_customer_plan(iq, code)
142			ParentCodeRepo.new(redis: REDIS, db: DB).find(code).then do |parent|
143				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
144				@customer = @customer.with_plan(plan.name, parent_customer_id: parent)
145				@customer.save_plan!
146			end
147		end
148
149		class GooglePlay
150			def initialize(customer, google_play_userid, tel)
151				@customer = customer
152				@google_play_userid = google_play_userid
153				@tel = tel
154				@invites = InvitesRepo.new(DB, REDIS)
155				@parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
156			end
157
158			def used
159				REDIS.sismember("google_play_userids", @google_play_userid)
160			end
161
162			def form
163				FormTemplate.render(
164					"registration/google_play",
165					tel: @tel
166				)
167			end
168
169			def write
170				used.then do |u|
171					next Activation.for(@customer, nil, @tel).write if u.to_s == "1"
172
173					Command.reply { |reply|
174						reply.allowed_actions = [:next]
175						reply.command << form
176					}.then(&method(:activate)).then do
177						Finish.new(@customer, @tel).write
178					end
179				end
180			end
181
182			def activate(iq)
183				plan = Plan.for_registration(iq.form.field("plan_name").value)
184				code = iq.form.field("code")&.value
185				EMPromise.all([
186					@parent_code_repo.find(code),
187					REDIS.sadd("google_play_userids", @google_play_userid)
188				]).then { |(parent, _)|
189					save_active_plan(plan, parent)
190				}.then do
191					use_referral_code(code)
192				end
193			end
194
195		protected
196
197			def save_bogus_transaction
198				Transaction.new(
199					customer_id: @customer.customer_id,
200					transaction_id: "google_play_#{@customer.customer_id}",
201					amount: 0,
202					note: "Activated via Google Play",
203					bonus_eligible?: false
204				).insert
205			end
206
207			def save_active_plan(plan, parent)
208				@customer = @customer.with_plan(plan.name, parent_customer_id: parent)
209				save_bogus_transaction.then do
210					@customer.activate_plan_starting_now
211				end
212			end
213
214			def use_referral_code(code)
215				EMPromise.resolve(nil).then {
216					@invites.claim_code(@customer.customer_id, code) {
217						@customer.extend_plan
218					}
219				}.catch_only(InvitesRepo::Invalid) do
220					@invites.stash_code(@customer.customer_id, code)
221				end
222			end
223		end
224
225		class Allow < Activation
226			def self.for(customer, tel, jid)
227				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
228				new(customer, tel, credit_to)
229			end
230
231			def initialize(customer, tel, credit_to)
232				super(customer, tel)
233				@credit_to = credit_to
234			end
235
236			def form
237				FormTemplate.render(
238					"registration/allow",
239					tel: tel,
240					domain: customer.jid.domain
241				)
242			end
243
244			def next_step(iq)
245				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
246				@customer = customer.with_plan(plan.name)
247				EMPromise.resolve(nil).then { activate }.then do
248					Finish.new(customer, tel).write
249				end
250			end
251
252		protected
253
254			def activate
255				DB.transaction do
256					if @credit_to
257						InvitesRepo.new(DB, REDIS).create_claimed_code(
258							@credit_to,
259							customer.customer_id
260						)
261					end
262					@customer.activate_plan_starting_now
263				end
264			end
265		end
266	end
267
268	module Payment
269		def self.kinds
270			@kinds ||= {}
271		end
272
273		def self.for(iq, customer, tel, final_message: nil, finish: Finish)
274			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
275				raise "Invalid activation method"
276			}.call(customer, tel, final_message: final_message, finish: finish)
277		end
278
279		class Bitcoin
280			Payment.kinds[:bitcoin] = method(:new)
281
282			THIRTY_DAYS = 60 * 60 * 24 * 30
283
284			def initialize(customer, tel, final_message: nil, **)
285				@customer = customer
286				@customer_id = customer.customer_id
287				@tel = tel
288				@final_message = final_message
289			end
290
291			attr_reader :customer_id, :tel
292
293			def save
294				TEL_SELECTION.set(@customer.jid, @tel)
295			end
296
297			def form(rate, addr)
298				amount = CONFIG[:activation_amount] / rate
299
300				FormTemplate.render(
301					"registration/btc",
302					amount: amount,
303					addr: addr,
304					final_message: @final_message
305				)
306			end
307
308			def write
309				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
310					Command.reply { |reply|
311						reply.allowed_actions = [:prev]
312						reply.status = :canceled
313						reply.command << form(rate, addr)
314					}.then(&method(:handle_possible_prev))
315				end
316			end
317
318		protected
319
320			def handle_possible_prev(iq)
321				raise "Action not allowed" unless iq.prev?
322
323				Activation.for(@customer, nil, @tel).then(&:write)
324			end
325
326			def addr_and_rate
327				EMPromise.all([
328					@customer.btc_addresses.then { |addrs|
329						addrs.first || @customer.add_btc_address
330					},
331					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
332				])
333			end
334		end
335
336		class CreditCard
337			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
338
339			def self.for(in_customer, tel, finish: Finish, **)
340				reload_customer(in_customer).then do |(customer, payment_methods)|
341					if customer.balance >= CONFIG[:activation_amount_accept]
342						next BillPlan.new(customer, tel, finish: finish)
343					end
344
345					if (method = payment_methods.default_payment_method)
346						next Activate.new(customer, method, tel, finish: finish)
347					end
348
349					new(customer, tel, finish: finish)
350				end
351			end
352
353			def self.reload_customer(customer)
354				EMPromise.all([
355					Command.execution.customer_repo.find(customer.customer_id),
356					customer.payment_methods
357				])
358			end
359
360			def initialize(customer, tel, finish: Finish)
361				@customer = customer
362				@tel = tel
363				@finish = finish
364			end
365
366			def oob(reply)
367				oob = OOB.find_or_create(reply.command)
368				oob.url = CONFIG[:credit_card_url].call(
369					reply.to.stripped.to_s.gsub("\\", "%5C"),
370					@customer.customer_id
371				) + "&amount=#{CONFIG[:activation_amount]}"
372				oob.desc = "Add credit card, save, then next here to continue"
373				oob
374			end
375
376			def write
377				Command.reply { |reply|
378					reply.allowed_actions = [:next, :prev]
379					toob = oob(reply)
380					reply.note_type = :info
381					reply.note_text = "#{toob.desc}: #{toob.url}"
382				}.then do |iq|
383					next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
384
385					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
386				end
387			end
388
389			class Activate
390				def initialize(customer, payment_method, tel, finish: Finish)
391					@customer = customer
392					@payment_method = payment_method
393					@tel = tel
394					@finish = finish
395				end
396
397				def write
398					CreditCardSale.create(
399						@customer,
400						amount: CONFIG[:activation_amount],
401						payment_method: @payment_method
402					).then(
403						->(_) { sold },
404						->(_) { declined }
405					)
406				end
407
408			protected
409
410				def sold
411					BillPlan.new(@customer, @tel, finish: @finish).write
412				end
413
414				DECLINE_MESSAGE =
415					"Your bank declined the transaction. " \
416					"Often this happens when a person's credit card " \
417					"is a US card that does not support international " \
418					"transactions, as JMP is not based in the USA, though " \
419					"we do support transactions in USD.\n\n" \
420					"You may add another card"
421
422				def decline_oob(reply)
423					oob = OOB.find_or_create(reply.command)
424					oob.url = CONFIG[:credit_card_url].call(
425						reply.to.stripped.to_s.gsub("\\", "%5C"),
426						@customer.customer_id
427					) + "&amount=#{CONFIG[:activation_amount]}"
428					oob.desc = DECLINE_MESSAGE
429					oob
430				end
431
432				def declined
433					Command.reply { |reply|
434						reply_oob = decline_oob(reply)
435						reply.allowed_actions = [:next]
436						reply.note_type = :error
437						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
438					}.then do
439						CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
440					end
441				end
442			end
443		end
444
445		class InviteCode
446			Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) }
447
448			def self.for(in_customer, tel, finish: Finish, **)
449				reload_customer(in_customer).then do |customer|
450					if customer.balance >= CONFIG[:activation_amount_accept]
451						next BillPlan.new(customer, tel, finish: finish)
452					end
453
454					msg = if customer.balance.positive?
455						"Account balance not enough to cover the activation"
456					end
457					new(customer, tel, error: msg, finish: Finish)
458				end
459			end
460
461			def self.reload_customer(customer)
462				Command.execution.customer_repo.find(customer.customer_id)
463			end
464
465			FIELDS = [{
466				var: "code",
467				type: "text-single",
468				label: "Your referral code",
469				required: true
470			}].freeze
471
472			def initialize(customer, tel, error: nil, finish: Finish, **)
473				@customer = customer
474				@tel = tel
475				@error = error
476				@finish = finish
477				@parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
478			end
479
480			def add_form(reply)
481				form = reply.form
482				form.type = :form
483				form.title = "Enter Referral Code"
484				form.instructions = @error if @error
485				form.fields = FIELDS
486			end
487
488			def write
489				Command.reply { |reply|
490					reply.allowed_actions = [:next, :prev]
491					add_form(reply)
492				}.then(&method(:parse))
493			end
494
495			def parse(iq)
496				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
497
498				verify(iq.form.field("code")&.value&.to_s)
499					.catch_only(InvitesRepo::Invalid, &method(:invalid_code))
500					.then(&:write)
501			end
502
503		protected
504
505			def invalid_code(e)
506				InviteCode.new(@customer, @tel, error: e.message)
507			end
508
509			def customer_id
510				@customer.customer_id
511			end
512
513			def verify(code)
514				@parent_code_repo.find(code).then do |parent_customer_id|
515					if parent_customer_id
516						set_parent(parent_customer_id)
517					else
518						InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) {
519							@customer.activate_plan_starting_now
520						}.then { Finish.new(@customer, @tel) }
521					end
522				end
523			end
524
525			def set_parent(parent_customer_id)
526				@customer = @customer.with_plan(
527					@customer.plan_name,
528					parent_customer_id: parent_customer_id
529				)
530				@customer.save_plan!.then do
531					self.class.for(@customer, @tel, finish: @finish)
532				end
533			end
534		end
535
536		class Mail
537			Payment.kinds[:mail] = method(:new)
538
539			def initialize(customer, tel, final_message: nil, **)
540				@customer = customer
541				@tel = tel
542				@final_message = final_message
543			end
544
545			def form
546				FormTemplate.render(
547					"registration/mail",
548					currency: @customer.currency,
549					final_message: @final_message,
550					**onboarding_extras
551				)
552			end
553
554			def onboarding_extras
555				jid = ProxiedJID.new(@customer.jid).unproxied
556				return {} unless jid.domain == CONFIG[:onboarding_domain]
557
558				{
559					customer_id: @customer.customer_id,
560					in_note: "Customer ID"
561				}
562			end
563
564			def write
565				Command.reply { |reply|
566					reply.allowed_actions = [:prev]
567					reply.status = :canceled
568					reply.command << form
569				}.then { |iq|
570					raise "Action not allowed" unless iq.prev?
571
572					Activation.for(@customer, nil, @tel).then(&:write)
573				}
574			end
575		end
576	end
577
578	class BillPlan
579		def initialize(customer, tel, finish: Finish)
580			@customer = customer
581			@tel = tel
582			@finish = finish
583		end
584
585		def write
586			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
587				@finish.new(@customer, @tel).write
588			end
589		end
590	end
591
592	class Finish
593		def initialize(customer, tel)
594			@customer = customer
595			@tel = tel
596			@invites = InvitesRepo.new(DB, REDIS)
597		end
598
599		def write
600			BandwidthTnReservationRepo.new.get(@customer, @tel.tel).then do |rid|
601				BandwidthTNOrder.create(
602					@tel.tel,
603					customer_order_id: @customer.customer_id,
604					reservation_id: rid
605				).then(&:poll).then(
606					->(_) { customer_active_tel_purchased },
607					method(:number_purchase_error)
608				)
609			end
610		end
611
612	protected
613
614		def number_purchase_error(e)
615			Command.log.error "number_purchase_error", e
616			TEL_SELECTIONS.delete(@customer.jid).then {
617				TEL_SELECTIONS[@customer.jid]
618			}.then { |choose|
619				choose.choose_tel(
620					error: "The JMP number #{@tel} is no longer available."
621				)
622			}.then { |tel| Finish.new(@customer, tel).write }
623		end
624
625		def raise_setup_error(e)
626			Command.log.error "@customer.register! failed", e
627			Command.finish(
628				"There was an error setting up your number, " \
629				"please contact JMP support.",
630				type: :error
631			)
632		end
633
634		def put_default_fwd
635			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
636				uri: "xmpp:#{@customer.jid}",
637				voicemail_enabled: true
638			))
639		end
640
641		def use_referral_code
642			@invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
643				next unless credit_to
644
645				Transaction.new(
646					customer_id: @customer.customer_id,
647					transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
648					amount: @customer.monthly_price,
649					note: "Referral Bonus",
650					bonus_eligible?: false
651				).insert
652			end
653		end
654
655		def customer_active_tel_purchased
656			@customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
657				EMPromise.all([
658					TEL_SELECTIONS.delete(@customer.jid),
659					put_default_fwd,
660					use_referral_code
661				])
662			}.then do
663				FinishOnboarding.for(@customer, @tel).then(&:write)
664			end
665		end
666	end
667
668	module FinishOnboarding
669		def self.for(customer, tel, db: LazyObject.new { DB })
670			jid = ProxiedJID.new(customer.jid).unproxied
671			if jid.domain == CONFIG[:onboarding_domain]
672				Snikket.for(customer, tel, db: db)
673			else
674				NotOnboarding.new(customer, tel)
675			end
676		end
677
678		class Snikket
679			def self.for(customer, tel, db:)
680				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
681					if is.empty?
682						new(customer, tel, db: db)
683					elsif is[0].bootstrap_token.empty?
684						# This is a need_dns one, try the launch again
685						new(customer, tel, db: db).launch(is[0].domain)
686					else
687						GetInvite.for(customer, is[0], tel, db: db)
688					end
689				end
690			end
691
692			def initialize(customer, tel, error: nil, old: nil, db:)
693				@customer = customer
694				@tel = tel
695				@error = error
696				@db = db
697				@old = old
698			end
699
700			ACTION_VAR = "http://jabber.org/protocol/commands#actions"
701
702			def form
703				FormTemplate.render(
704					"registration/snikket",
705					tel: @tel,
706					error: @error
707				)
708			end
709
710			def write
711				Command.reply { |reply|
712					reply.allowed_actions = [:next]
713					reply.command << form
714				}.then(&method(:next_step))
715			end
716
717			def next_step(iq)
718				subdomain = empty_nil(iq.form.field("subdomain")&.value)
719				domain = "#{subdomain}.snikket.chat"
720				if iq.form.field(ACTION_VAR)&.value == "custom_domain"
721					CustomDomain.new(@customer, @tel, old: @old).write
722				elsif @old && (!subdomain || domain == @old.domain)
723					GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
724				else
725					launch(domain)
726				end
727			end
728
729			def launch(domain)
730				IQ_MANAGER.write(::Snikket::Launch.new(
731					nil, CONFIG[:snikket_hosting_api], domain: domain
732				)).then { |launched|
733					save_instance_and_wait(domain, launched)
734				}.catch { |e|
735					next EMPromise.reject(e) unless e.respond_to?(:text)
736
737					Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
738				}
739			end
740
741			def save_instance_and_wait(domain, launched)
742				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
743				repo = ::Snikket::Repo.new(db: @db)
744				(@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
745					.then { repo.put(instance) }.then do
746						if launched.status == :needs_dns
747							NeedsDNS.new(@customer, instance, @tel, launched.records).write
748						else
749							GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
750						end
751					end
752			end
753
754			def empty_nil(s)
755				s.nil? || s.empty? ? nil : s
756			end
757
758			class NeedsDNS < Snikket
759				def initialize(customer, instance, tel, records, db: DB)
760					@customer = customer
761					@instance = instance
762					@tel = tel
763					@records = records
764					@db = db
765				end
766
767				def form
768					FormTemplate.render(
769						"registration/snikket_needs_dns",
770						records: @records
771					)
772				end
773
774				def write
775					Command.reply { |reply|
776						reply.allowed_actions = [:prev, :next]
777						reply.command << form
778					}.then do |iq|
779						if iq.prev?
780							CustomDomain.new(@customer, @tel, old: @instance).write
781						else
782							launch(@instance.domain)
783						end
784					end
785				end
786			end
787
788			class GetInvite
789				def self.for(customer, instance, tel, db: DB)
790					instance.fetch_invite.then do |xmpp_uri|
791						if xmpp_uri
792							GoToInvite.new(xmpp_uri)
793						else
794							new(customer, instance, tel, db: db)
795						end
796					end
797				end
798
799				def initialize(customer, instance, tel, db: DB)
800					@customer = customer
801					@instance = instance
802					@tel = tel
803					@db = db
804				end
805
806				def form
807					FormTemplate.render(
808						"registration/snikket_wait",
809						domain: @instance.domain
810					)
811				end
812
813				def write
814					Command.reply { |reply|
815						reply.allowed_actions = [:prev, :next]
816						reply.command << form
817					}.then do |iq|
818						if iq.prev?
819							Snikket.new(@customer, @tel, old: @instance, db: @db).write
820						else
821							GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
822						end
823					end
824				end
825			end
826
827			class GoToInvite
828				def initialize(xmpp_uri)
829					@xmpp_uri = xmpp_uri
830				end
831
832				def write
833					Command.finish do |reply|
834						oob = OOB.find_or_create(reply.command)
835						oob.url = @xmpp_uri
836					end
837				end
838			end
839		end
840
841		class CustomDomain < Snikket
842			def initialize(customer, tel, old: nil, error: nil, db: DB)
843				@customer = customer
844				@tel = tel
845				@error = error
846				@old = old
847				@db = db
848			end
849
850			def form
851				FormTemplate.render(
852					"registration/snikket_custom",
853					tel: @tel,
854					error: @error
855				)
856			end
857
858			def write
859				Command.reply { |reply|
860					reply.allowed_actions = [:prev, :next]
861					reply.command << form
862				}.then do |iq|
863					if iq.prev?
864						Snikket.new(@customer, @tel, db: @db, old: @old).write
865					else
866						launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
867					end
868				end
869			end
870		end
871
872		class NotOnboarding
873			def initialize(customer, tel)
874				@customer = customer
875				@tel = tel
876			end
877
878			def write
879				WelcomeMessage.new(@customer, @tel).welcome
880				Command.finish("Your JMP account has been activated as #{@tel}")
881			end
882		end
883	end
884end